[{"content":" 我们为什么需要 MVCC ？ 没有MVCC（多版本并发控制 Multi-Version Concurrency Control）的情况下，面对数据库事务中\u0026quot;读某一条数据\u0026quot;的行为，我们只能加锁。这个锁可以是共享锁/排他锁，保证\u0026quot;我读的时候没人过来写\u0026quot;，避免脏读（读到和最终结果不一致的数据）。然而一旦加锁，其他线程就要等，一等并发效率就直线下降。\nMVCC 的思路是：读的时候不加锁，而是通过\u0026quot;版本\u0026quot;来判断一条数据对我来说能不能读。\n每一行数据的三个隐藏字段ba 每一条数据除了业务字段外，还有三个隐藏字段：\nDB_TRX_ID 最近一次插入/更新/删除该行的事务id DB_ROLL_PTR 回滚指针，指向这行数据在 undo log 中的上一个版本 DB_ROW_ID 隐藏主键，仅在表没有定义主键时才会生成 undo log 把同一行数据的历史版本串成一条链，DB_ROLL_PTR 就是链上的指针，出问题时可以顺着它一路往回找老版本。\nRead View：读之前先拍个快照 在真正读数据之前，我们会先生成一个快照，叫做 read view。它记录了这几样东西：\nm_ids 生成快照那一刻，所有\u0026quot;活跃\u0026quot;（未提交）的事务id列表 min_trx_id m_ids 里最小的那个id max_trx_id 系统里下一个将要分配的事务id（也就是目前已知最大事务id + 1） 注意 max_trx_id 不是\u0026quot;当前活跃事务里最大的那个\u0026quot;，而是\u0026quot;还没被任何事务用过的、未来第一个可用的id\u0026quot;——因为事务id是全局递增分配的。\n这里要先说清楚一件容易被忽略的事：read view 什么时候生成，在不同隔离级别下是不一样的。\nREAD COMMITTED（读已提交）：每次执行 SELECT 语句都重新生成一个 read view REPEATABLE READ（可重复读，MySQL默认）：只在事务内第一次执行 SELECT 时生成一次，之后整个事务复用同一个 read view 这个区别直接决定了\u0026quot;我这次读，能不能看到别人事务提交的新数据\u0026quot;——RC 每次都能看到最新提交的结果，RR 则始终锁定在事务开始时的那个快照上，这也是可重复读名字的由来。\n拿到 read view 之后，怎么判断某一行能不能读 现在回到最初的问题：我们要读一行数据，它的 DB_TRX_ID = 200。这条数据被事务200修改过，我们能不能读到它，要按顺序做以下判断：\n判断1：如果 DB_TRX_ID 就是我自己当前事务的id，那肯定能读——是我自己改的，当然可见。\n判断2：如果 DB_TRX_ID \u0026lt; min_trx_id，说明这个事务在我拍快照之前就已经结束（提交或回滚）了，属于\u0026quot;很老的事务\u0026quot;，这个版本可见。\n判断3：如果 DB_TRX_ID ≥ max_trx_id，说明这个事务是在我生成快照之后才开始的，对我来说它是\u0026quot;未来\u0026quot;的事务，这个版本不可见。\n判断4：如果 min_trx_id ≤ DB_TRX_ID \u0026lt; max_trx_id，但这个id不在 m_ids 列表里，说明这个事务在我拍快照的时候已经提交了，这个版本可见。\n判断5：如果 DB_TRX_ID 正好在 m_ids 列表里，说明这个事务在我拍快照的那一刻还没提交，对我来说不可见。这种情况下，就顺着这一版本的 DB_ROLL_PTR 回退到 undo log 里的上一个版本，拿这个更老的版本重新走一遍上面的判断，直到找到一个可见的版本为止（如果一直回退到头都没有可见版本，说明这行数据对我来说还不存在）。\n小结 这套机制本质上是拿 undo log 存的历史版本链，换来了读不用加锁、写不阻塞读的高并发。代价也很实在：undo log 会随着未提交事务的存在不断膨胀，需要靠 purge 线程在没人再需要旧版本时把它们清理掉。如果长事务迟迟不提交，历史版本积压过多，也会拖慢查询、占用大量存储——这就是MVCC这顿\u0026quot;免费午餐\u0026quot;背后真正的账单。\n","permalink":"https://lv-blog.pages.dev/posts/programming/%E7%90%86%E8%A7%A3mysql%E7%9A%84mvcc%E9%82%A3%E4%B9%88%E7%AE%80%E5%8D%95%E4%B8%BA%E4%BB%80%E4%B9%88%E5%B0%B1%E6%B2%A1%E6%9C%89%E4%BA%BA%E8%AF%B4%E4%BA%BA%E8%AF%9D/","summary":"\u003cul\u003e\n\u003cli\u003e我们为什么需要 MVCC ？\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e没有MVCC（多版本并发控制 Multi-Version Concurrency Control）的情况下，面对数据库事务中\u0026quot;读某一条数据\u0026quot;的行为，我们只能加锁。这个锁可以是共享锁/排他锁，保证\u0026quot;我读的时候没人过来写\u0026quot;，避免脏读（读到和最终结果不一致的数据）。然而一旦加锁，其他线程就要等，一等并发效率就直线下降。\u003c/p\u003e\n\u003cp\u003eMVCC 的思路是：读的时候不加锁，而是通过\u0026quot;版本\u0026quot;来判断一条数据对我来说能不能读。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e每一行数据的三个隐藏字段ba\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e每一条数据除了业务字段外，还有三个隐藏字段：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003eDB_TRX_ID   最近一次插入/更新/删除该行的事务id\nDB_ROLL_PTR 回滚指针，指向这行数据在 undo log 中的上一个版本\nDB_ROW_ID   隐藏主键，仅在表没有定义主键时才会生成\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003eundo log 把同一行数据的历史版本串成一条链，DB_ROLL_PTR 就是链上的指针，出问题时可以顺着它一路往回找老版本。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eRead View：读之前先拍个快照\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e在真正读数据之前，我们会先生成一个快照，叫做 read view。它记录了这几样东西：\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003em_ids       生成快照那一刻，所有\u0026quot;活跃\u0026quot;（未提交）的事务id列表\nmin_trx_id  m_ids 里最小的那个id\nmax_trx_id  系统里下一个将要分配的事务id（也就是目前已知最大事务id + 1）\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e注意 max_trx_id 不是\u0026quot;当前活跃事务里最大的那个\u0026quot;，而是\u0026quot;还没被任何事务用过的、未来第一个可用的id\u0026quot;——因为事务id是全局递增分配的。\u003c/p\u003e\n\u003cp\u003e这里要先说清楚一件容易被忽略的事：\u003cstrong\u003eread view 什么时候生成，在不同隔离级别下是不一样的。\u003c/strong\u003e\u003c/p\u003e\n\u003cpre\u003e\u003ccode\u003eREAD COMMITTED（读已提交）：每次执行 SELECT 语句都重新生成一个 read view\nREPEATABLE READ（可重复读，MySQL默认）：只在事务内第一次执行 SELECT 时生成一次，之后整个事务复用同一个 read view\n\u003c/code\u003e\u003c/pre\u003e\n\u003cp\u003e这个区别直接决定了\u0026quot;我这次读，能不能看到别人事务提交的新数据\u0026quot;——RC 每次都能看到最新提交的结果，RR 则始终锁定在事务开始时的那个快照上，这也是可重复读名字的由来。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e拿到 read view 之后，怎么判断某一行能不能读\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e现在回到最初的问题：我们要读一行数据，它的 DB_TRX_ID = 200。这条数据被事务200修改过，我们能不能读到它，要按顺序做以下判断：\u003c/p\u003e","title":"理解MySQL的MVCC"},{"content":"生活相关 穷的时候没钱乱买东西，正好也没地方放。。因为穷所以租的房子小，购物欲和收纳空间同步受限，消费主义完成了自我抵消； 辣的东西容易吃多，但辣到一定程度嘴会痛到吃不下去——过量摄入的风险被摄入过程本身劝退了，自带限流熔断； 越常用的汉字笔画越简单（\u0026ldquo;的、一、是、了\u0026rdquo;），越生僻的字才越复杂。使用频率和书写成本完美负相关，语言自己完成了性能优化； 睡眠充足的人精力好，精力好就更有自制力早睡，早睡又保证了睡眠充足——自律的人连生物钟都在帮他滚雪球，简直完美； 脸皮薄、不好意思拒绝别人的人，往往朋友越来越多（都觉得他好说话），结果他独处恢复精力的时间越来越少——社交资源与心理能量天然对冲，性格自己给自己设了天花板。当一个人的社交资源有了新的高度，就必然要牺牲内在的深度； 水在4℃时密度最大，所以冬天湖面结冰而湖底保持4℃，鱼不会冻死； 哲学相关 维基百科的词条越中立，读起来越无聊；越有立场的词条，读起来越生动，但越不可靠——可信度与传播力在信息熵里互相拉扯，真相常常干不过叙事； 越是深奥的知识说得越绕，越绕的话越没人愿意读完——高深的知识自带传播衰减（好像跟上一条差不多哈😑）； 摄影相关 相机光圈更大，进光量更大，画质会变得更好，快门速度也可以更快。对于追求氛围感的摄影，还能带来极致的虚化感，简直完美。如果是拒绝虚化效果的写实摄影，那就没这个好处了； 胶片按一张亏一张，所以每次抬手都要想清楚，出片率反而比数码高——成本是最好的构图老师； 编程相关 Kafka 消息可能重复投递是个缺点，但这逼着你把消费者写成幂等的；写成幂等之后，重试、宕机恢复、手动补数据全都不怕了。一个不可靠性逼出了一整套可靠性； JAVA使用递归，代码让人看得头大，但是递归又有StackOverFlow的隐患，一般好的程序员不会写递归代码，正好解决了读递归代码脑壳疼的问题； 布隆过滤器会误判\u0026quot;存在\u0026quot;，但绝不误判\u0026quot;不存在\u0026quot;，而挡缓存穿透需要的正是后者——一个不精确的结构在这个场景里精确得刚刚好； 开源项目用的人越多，bug 被发现修得越快，越稳定就越多人用 ","permalink":"https://lv-blog.pages.dev/posts/thoughts/%E6%9D%A5%E6%80%BB%E7%BB%93%E4%B8%80%E4%B8%8B%E4%B8%A4%E7%9B%B8%E5%AE%8C%E7%BE%8E%E7%9A%84%E4%BA%8B%E6%83%85/","summary":"\u003ch2 id=\"生活相关\"\u003e生活相关\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e穷的时候没钱乱买东西，正好也没地方放。。因为穷所以租的房子小，购物欲和收纳空间同步受限，消费主义完成了自我抵消；\u003c/li\u003e\n\u003cli\u003e辣的东西容易吃多，但辣到一定程度嘴会痛到吃不下去——过量摄入的风险被摄入过程本身劝退了，自带限流熔断；\u003c/li\u003e\n\u003cli\u003e越常用的汉字笔画越简单（\u0026ldquo;的、一、是、了\u0026rdquo;），越生僻的字才越复杂。使用频率和书写成本完美负相关，语言自己完成了性能优化；\u003c/li\u003e\n\u003cli\u003e睡眠充足的人精力好，精力好就更有自制力早睡，早睡又保证了睡眠充足——自律的人连生物钟都在帮他滚雪球，简直完美；\u003c/li\u003e\n\u003cli\u003e脸皮薄、不好意思拒绝别人的人，往往朋友越来越多（都觉得他好说话），结果他独处恢复精力的时间越来越少——社交资源与心理能量天然对冲，性格自己给自己设了天花板。当一个人的社交资源有了新的高度，就必然要牺牲内在的深度；\u003c/li\u003e\n\u003cli\u003e水在4℃时密度最大，所以冬天湖面结冰而湖底保持4℃，鱼不会冻死；\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"哲学相关\"\u003e哲学相关\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e维基百科的词条越中立，读起来越无聊；越有立场的词条，读起来越生动，但越不可靠——可信度与传播力在信息熵里互相拉扯，真相常常干不过叙事；\u003c/li\u003e\n\u003cli\u003e越是深奥的知识说得越绕，越绕的话越没人愿意读完——高深的知识自带传播衰减（好像跟上一条差不多哈😑）；\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"摄影相关\"\u003e摄影相关\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e相机光圈更大，进光量更大，画质会变得更好，快门速度也可以更快。对于追求氛围感的摄影，还能带来极致的虚化感，简直完美。如果是拒绝虚化效果的写实摄影，那就没这个好处了；\u003c/li\u003e\n\u003cli\u003e胶片按一张亏一张，所以每次抬手都要想清楚，出片率反而比数码高——成本是最好的构图老师；\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"编程相关\"\u003e编程相关\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eKafka 消息可能重复投递是个缺点，但这逼着你把消费者写成幂等的；写成幂等之后，重试、宕机恢复、手动补数据全都不怕了。一个不可靠性逼出了一整套可靠性；\u003c/li\u003e\n\u003cli\u003eJAVA使用递归，代码让人看得头大，但是递归又有StackOverFlow的隐患，一般好的程序员不会写递归代码，正好解决了读递归代码脑壳疼的问题；\u003c/li\u003e\n\u003cli\u003e布隆过滤器会误判\u0026quot;存在\u0026quot;，但绝不误判\u0026quot;不存在\u0026quot;，而挡缓存穿透需要的正是后者——一个不精确的结构在这个场景里精确得刚刚好；\u003c/li\u003e\n\u003cli\u003e开源项目用的人越多，bug 被发现修得越快，越稳定就越多人用\u003c/li\u003e\n\u003c/ul\u003e","title":"来，总结一下我遇到的完美对冲"},{"content":"在我的项目“过家家”里，有一个抢红包的功能。功能类似微信抢红包。\n抢红包的基本逻辑 v1（低并发）抢红包时实时使用二倍均值法计算，只使用MySQL 1. 红包创建流程 用户发红包时，指定总金额与红包个数 系统将总金额拆分为指定数量的红包 红包数量约束： 最少为 1 个 最多不能超过群成员数量 2. 红包金额分配策略 单个红包：不进行随机计算，抢到即获得全部金额 多个红包：采用二倍均值法进行随机分配 当前用户最大可抢金额 = 剩余金额 / 剩余人数 × 2 实际可抢区间为 [0.01, 最大可抢金额 - 0.01] 下限 0.01 元：保证每个抢到的用户都有收益 上限减 0.01 元：保证剩余红包仍有余额可抢 边际示例：10 元红包 2 人分，第 1 人可抢区间为 [0.01, 9.99] 3. 数据持久化与防刷控制 红包创建后入库存储 发红包限流：同一用户 10 秒内仅允许发送 1 个红包，防止手抖误操作 4. 抢红包并发控制 防重复抢：在 redpacket_grabs 表中建立 (userId, redpacketId) 联合唯一索引，确保同一用户对同一红包仅能抢一次 防超卖：采用数据库乐观锁机制 更新 redpacket 表时，必须同时满足： remaining_count \u0026gt; 0 remaining_amount \u0026gt;= 本次扣减金额 5. 方法缺点 每次请求直接打数据库，导致响应时间没有访问内存快 每次请求都会使用二分均值法计算抢到的金额，并且做超卖判断，比较麻烦 v2 （高并发）红包创建好了就已经固定了抢红包的数量和个数，加入Redis 大致流程\n创建红包时：预热，红包信息塞进 redis 抢红包时：Lua原子性 抢完：异步落库，把哪个用户抢了多少慢慢写进DB 具体流程\n创建红包 限流检查：同一用户10秒内仅允许发送1个红包 预拆分金额：使用二倍均值法将总金额拆分为N个子红包（单位：分） 写入Redis： 子红包金额列表 → RPUSH redpacket:{id}:amounts（元素为金额） 红包元信息 → HSET redpacket:{id}:info（总金额、个数、状态等） 已抢记录 → 初始化空Hash redpacket:{id}:grabbed 异步落库：将红包主数据写入MySQL（状态标记为pending） 抢红包（Lua原子操作） 执行Lua脚本（原子执行三步）：\n防重复检查：HEXISTS redpacket:{id}:grabbed {userId}，已抢过则返回 -1 弹出金额：LPOP redpacket:{id}:amounts，列表为空则返回 -2（已抢完） 记录抢到信息：HSET redpacket:{id}:grabbed {userId} {amount} 返回结果：成功返回 [1, 金额（分）]，失败返回 [-1, 0] 或 [-2, 0]\n异步落库（最终一致性） 写入队列：抢红包成功后，将记录 (userId, redpacketId, amount, time) 推入消息队列或内存队列 批量刷库：定时任务（每5秒或积压≥100条）批量插入MySQL： redpacket_grabs 表（抢红包记录） 更新 redpacket 表（剩余数量、剩余金额、状态） 异常补偿：落库失败记录日志，定时重试或对账修复 二倍均值法 要解决的问题 红包总金额固定 红包总个数固定 每个人抢到的金额随机 所有人抢完后，金额总和等于红包总金额 核心公式 当前用户最大可抢金额 = 剩余金额 / 剩余人数 × 2\n边界说明 实际业务中需要增加上下限约束：\n下限：0.01 元（不能让用户抢到 0 元） 上限：剩余金额 / 剩余人数 × 2 - 0.01（必须为后续红包留出最少 0.01 元） 为什么上限要减 0.01？\n如果第一个人把上限额度抢完，剩余金额恰好只够剩余人数每人 0.01 元，这样是可以的。\n但如果上限不减 0.01，可能出现剩余金额为 0 的情况，导致后面的人抢不到钱。\n举例：1000 分 3 人分，若第一个抢到 666 分（1000/3×2 ≈ 666），剩余 334 分，剩下两人每人最多 167 分，仍然 \u0026gt; 0.01，看起来没问题。\n但考虑极端情况：假设剩余金额为 300 分，剩余 2 人，按公式最大可抢 = 300/2×2 = 300 分。如果第一个人真的抢到 300 分，剩余 0 分，第二个人就无钱可抢了。因此上限必须减 0.01，即最多抢 299 分，保证第二个人至少有 1 分钱。\n完整抢红包流程 示例：发一个 1000 分（10 元）红包给 3 个人\n第一个人抢\n剩余金额：1000 分，剩余人数：3 人 最大可抢 = 1000 / 3 × 2 ≈ 666 分，上限再减 1 分 = 665 分 实际可抢区间：[1, 665] 分 假设抢到 100 分，剩余 900 分 第二个人抢\n剩余金额：900 分，剩余人数：2 人 最大可抢 = 900 / 2 × 2 - 1 = 899 分 实际可抢区间：[1, 899] 分 假设抢到 500 分，剩余 400 分 第三个人抢\n剩余人数为 1，直接拿走全部剩余金额（不进行随机计算） 获得 400 分 边界情况演示 极端场景：第一个人每次都抢上限，看最后一人是否仍有金额可拿\n第一个人抢\n剩余金额：1000 分，剩余人数：3 人 最大可抢 = 1000 / 3 × 2 - 1 ≈ 665 分 抢到上限 665 分，剩余 335 分 第二个人抢\n剩余金额：335 分，剩余人数：2 人 最大可抢 = 335 / 2 × 2 - 1 = 334 分 抢到上限 334 分，剩余 1 分 第三个人抢\n剩余人数为 1，直接拿走全部剩余金额 获得 1 分 ✅ 验证通过：即使前两人每次都抢到上限，最后一人仍有 0.01 元保底，不会出现无钱可抢的情况。\n为什么叫\u0026quot;二倍均值法\u0026quot;？ 项目 说明 当前剩余人均金额 剩余金额 / 剩余人数 随机上限 人均金额 × 2 名字由来 每次随机的上限是当前人均金额的 2 倍 这样可以保证：\n前面的人不会一次性把红包抢光（最多只能拿人均的 2 倍） 后面的人始终有剩余金额可抢 金额分布相对均匀，不会出现极端悬殊的情况 多线程下的 random 问题 以及 为什么用 ThreadLocalRandom ? random 的底层是 nextSeed() 方法，它使用了一个 AtomicLong 来保存随机种子Seed。多线程环境下，每次生成随机数都会尝试使用 CAS（Compare And Swap）操作去更新这个种子值。由于CAS同一时刻下只有一个线程能成功，其他线程会陷入自旋重试，导致CPU开销增加 AtomicLong 是 java.util.concurrent.atomic 包下的一个类，它提供了一种线程安全、非阻塞的方式来操作 long 类型的值，保证操作 long类型变量 的原子性。在多线程环境下，它可以替代 volatile + synchronized 的组合，实现高效的并发计数或ID生成。\nThreadLocalRandom 是 Random 的子类，但是采取了种子隔离机制，每个线程都有自己的种子副本，实现了无竞争，零阻塞。缺点就是占用一些内存空间。 一个有意思的问题 在二倍均值法中，每个用户抢到高额红包的概率似乎并不确定。在之前的例子中：\n发一个1000分（10元）的红包给3个人。\n第一个人抢，红包剩余1000分，该用户最多可得到665分，最少可以拿到1分。\n第一个用户的随机范围是 1-665，似乎比其他用户大得多。一旦他拿到了665分的金额，那么其他人只能“吃瘪”了。但是实际真的是这样吗？\n非也。\n第一个用户获得的随机金额，本质上主宰了整个抢红包游戏的随机性，但我们不能把“A用户抢红包的随机范围大”就等同于“A用户抢到高额红包的概率更高”。\n这里的关键在于 “概率的传递性”：\n如果 A 抢到了 1分钱，那么他把 高额红包的概率 传递给了 B 和 C。此时剩余金额 999 分，B 和 C 的随机上限会变高，大概率会诞生一个大包。 如果 A 抢到了 最高金额（665分），那么他把 低额红包的概率 传递给了 B 和 C。剩余金额只有 335 分，B 和 C 即便拿满，也远低于 A 的金额。 如果 A 抢到了一个 中间值，那么 B 和 C 就在剩余金额中继续博弈，概率继续向下传递。 所以整体来看，每个人拿到任意金额的期望概率其实是相等的。A 有概率拿到大包，但同时也承担了把“大包机会”让给后者的风险；后者虽然随机范围小，但承接的是前者“没拿完”的剩余价值。\n我们之所以会产生“先抢更占优势”的错觉，是因为我们只看到了“第一个人的随机范围大”，却忽略了“概率的传递性”和“剩余金额的守恒”。 就像三个人分一块蛋糕，第一个人切得多，后面就分得少；第一个人切得少，后面就分得多。切的人有选择权，但最终每个人吃到的蛋糕总量，在无数次重复后是相等的。\n","permalink":"https://lv-blog.pages.dev/posts/programming/backend/%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E6%8A%A2%E7%BA%A2%E5%8C%85/","summary":"\u003cp\u003e在我的项目“过家家”里，有一个抢红包的功能。功能类似微信抢红包。\u003c/p\u003e\n\u003ch2 id=\"抢红包的基本逻辑\"\u003e抢红包的基本逻辑\u003c/h2\u003e\n\u003ch3 id=\"v1低并发抢红包时实时使用二倍均值法计算只使用mysql\"\u003ev1（低并发）抢红包时实时使用二倍均值法计算，只使用MySQL\u003c/h3\u003e\n\u003ch4 id=\"1-红包创建流程\"\u003e1. 红包创建流程\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e用户发红包时，指定总金额与红包个数\u003c/li\u003e\n\u003cli\u003e系统将总金额拆分为指定数量的红包\u003c/li\u003e\n\u003cli\u003e红包数量约束：\n\u003cul\u003e\n\u003cli\u003e最少为 1 个\u003c/li\u003e\n\u003cli\u003e最多不能超过群成员数量\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"2-红包金额分配策略\"\u003e2. 红包金额分配策略\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e单个红包\u003c/strong\u003e：不进行随机计算，抢到即获得全部金额\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e多个红包\u003c/strong\u003e：采用二倍均值法进行随机分配\n\u003cul\u003e\n\u003cli\u003e当前用户最大可抢金额 = 剩余金额 / 剩余人数 × 2\u003c/li\u003e\n\u003cli\u003e实际可抢区间为 [0.01, 最大可抢金额 - 0.01]\n\u003cul\u003e\n\u003cli\u003e下限 0.01 元：保证每个抢到的用户都有收益\u003c/li\u003e\n\u003cli\u003e上限减 0.01 元：保证剩余红包仍有余额可抢\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e边际示例：10 元红包 2 人分，第 1 人可抢区间为 [0.01, 9.99]\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"3-数据持久化与防刷控制\"\u003e3. 数据持久化与防刷控制\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e红包创建后入库存储\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e发红包限流\u003c/strong\u003e：同一用户 10 秒内仅允许发送 1 个红包，防止手抖误操作\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"4-抢红包并发控制\"\u003e4. 抢红包并发控制\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e防重复抢\u003c/strong\u003e：在 \u003ccode\u003eredpacket_grabs\u003c/code\u003e 表中建立 \u003ccode\u003e(userId, redpacketId)\u003c/code\u003e 联合唯一索引，确保同一用户对同一红包仅能抢一次\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e防超卖\u003c/strong\u003e：采用数据库乐观锁机制\n\u003cul\u003e\n\u003cli\u003e更新 \u003ccode\u003eredpacket\u003c/code\u003e 表时，必须同时满足：\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eremaining_count \u0026gt; 0\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eremaining_amount \u0026gt;= 本次扣减金额\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch4 id=\"5-方法缺点\"\u003e5. 方法缺点\u003c/h4\u003e\n\u003cul\u003e\n\u003cli\u003e每次请求直接打数据库，导致响应时间没有访问内存快\u003c/li\u003e\n\u003cli\u003e每次请求都会使用二分均值法计算抢到的金额，并且做超卖判断，比较麻烦\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"v2-高并发红包创建好了就已经固定了抢红包的数量和个数加入redis\"\u003ev2 （高并发）红包创建好了就已经固定了抢红包的数量和个数，加入Redis\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e大致流程\u003c/strong\u003e\u003c/p\u003e","title":"过家家项目：如何设计抢红包"},{"content":"如果我们死后去哪里被科学家揭晓了，世界会发生什么？\n知晓答案的我们会迎来灾厄还是幸福？\n我能想到的几种答案：\n答案1：原来我们死后会进入一个等待队列，会在这个星球上失去一切记忆地随机再次以人形重生 按当前地球人口的增速来看，可能还没到\u0026quot;需要等\u0026quot;的那一步，我们一领便当，便当都没来得及吃，就从另一个母亲的肚子里哇哇落地了。\n如果重生被证明无需任何代价，即，你刚没意识，就突然从另一个人肚子里醒来。那么这会迎来几个相当有可能的后果：\n后果1（火速重开版） 长到知道什么是财富差距的年龄，发现自己家里一贫如洗，赶紧找把枪把自己枪毙。人生变成一场无限次数的抽卡游戏，而\u0026quot;重开\u0026quot;这个梗第一次拥有了字面意义。各国紧急立法管制一切危险物品，但拦不住——你没法用\u0026quot;珍惜生命\u0026quot;去劝一个知道生命是复读机的人。\n后果2（恶人的崛起） 反正人一辈子就是一死一循环的事，赶紧把坏事做尽，死比坐牢强。刑法的威慑力一夜清零：无期徒刑？我下一世照样是个白白净净的婴儿。死刑甚至成了越狱手段。人类引以为傲的整套惩罚体系，原来押的全是\u0026quot;人只活一次\u0026quot;这一个注。\n后果3（世界和平共同富裕） 原来我们无论如何都要重返这个世上，我们应该互帮互助，每个人都来得开心去得开心，没有战争，没有争抢，因为不必，我们只需开心地永恒着。你今天往河里倒的垃圾，五十年后会流进你下一世的水杯；你今天投下的炸弹，炸的可能是你自己明天的产房。环保和和平不再需要道德说教——它们成了纯粹的、自私的、无可辩驳的利己行为。\n这三种后果会同时发生。人类大概要用两百年惨烈地筛选，最后活下来的文明版本，多半是后果3。不是因为人性本善，而是因为前两种玩法的玩家，会一遍一遍重生在被自己搞烂的世界里，直到玩明白为止。\n答案2：还是随机重生，但是谁跟你说你还是人的形态了？ 重生池是全物种共享的。按生物量算一算就会脊背发凉：地球上全人类不到六亿吨，蚂蚁跟我们差不多重，线虫、浮游生物、细菌加起来是我们的几千倍。也就是说，在轮回抽卡池里，\u0026ldquo;人类\u0026quot;是个出货率低到离谱的SSR——你这一世能坐在这里刷手机，已经是欧皇本皇了。\n后果1（全民吃素版） 牛肉面馆一夜倒闭，因为那碗牛肉有万分之一的概率是你上一世的班主任。拍死一只蚊子要不要按\u0026quot;过失\u0026quot;处理成了立法难题，夏天变得极其难熬。渔业畜牧业连夜转型，人类食谱倒退回采集时代——好在植物暂时还没被查出在池子里。\n后果2（体验派的狂欢） 也有人开始期待死亡：\u0026ldquo;下辈子想抽个信天翁，环游世界不用签证。\u0026ldquo;\u0026ldquo;我想当一棵红杉，站两千年，看你们轮回。\u0026ldquo;殡仪馆的悼词从\u0026quot;一路走好\u0026quot;变成\u0026quot;祝你出货\u0026rdquo;。\n后果3（人类中心主义崩塌） \u0026ldquo;众生平等\u0026quot;从宗教口号变成物理事实。动物园关门，实验室白鼠获得赔偿性放生。人类第一次意识到：我们不是地球的主角，只是暂时借了一副带拇指的身体。\n答案3：还是随机重生，但是谁跟你说是在地球了？ 重生范围：全宇宙。可观测宇宙少说两万亿个星系，你重生回地球的概率，约等于连续一百次中彩票头奖。\n后果1（天文学成为殡葬行业） 天文台门口排起长队，家属握着天文学家的手：\u0026ldquo;您看我妈她……大概往哪个方向去了？\u0026ldquo;墓碑不再朝南，改朝室女座超星系团。清明烧纸的时候顺便烧一台射电望远镜模型，聊胜于无。\n后果2（宇宙级躺平） 大家突然不焦虑了。房贷？内卷？KPI？下辈子我可能是猎户座旋臂上一坨快乐的星际气体，在零下两百七十度里舒展十亿年。这辈子这点破事，格局小了。\n后果3（宇宙级恐慌） 也有人算了另一笔账：宇宙的绝大部分是真空、辐射和孤独。重生成一颗在星系间漂流一百亿年的氢原子——这到底算重生，还是无期徒刑？哲学系连夜开题：《论\u0026quot;存在\u0026quot;是否必须以\u0026quot;体验\u0026quot;为前提》。答辩没人通过，因为没人敢下结论。\n答案4：完了，发现原来我们一生做的好事坏事真的有积分系统计算着 后果1（功德通货膨胀） 斑马线前扶老奶奶的队伍排出两公里，老奶奶成为稀缺资源，出现职业\u0026quot;被扶员\u0026rdquo;，时薪不菲。公交车让座要靠抢，献血站限流，捐款平台崩溃。善良第一次出现了产能过剩。\n后果2（动机审查悖论） 科学家紧急补充说明：系统会识别动机，刷分无效。全人类当场哀嚎——那我现在做好事，到底是出于善良，还是出于\u0026quot;想让系统觉得我出于善良\u0026rdquo;，还是\u0026quot;知道系统会审查动机所以努力让自己真诚\u0026quot;的三阶套娃？这成为新时代哲学第一难题，康德全网售罄。\n后果3（历史清算） 有人翻出自己一生的流水账，发现小学偷拿同桌一块橡皮被扣了2分，至今没涨回来，当场崩溃。也有人发现自己随手做的一件小事——雨天给流浪猫挪了个纸箱——加的分比捐一栋楼还多。人们终于摸清了系统的口味：它不看金额，看你当时以为没人看见。\n后果4（宗教界的复杂心情） 各大宗教一半在庆祝\u0026quot;早说过了\u0026rdquo;，一半在紧急对账——教义里的计分规则和实测数据对不上，误差还不小。神学院连夜改开数据分析课。\n答案5：完了，原来所有的你你我我，最后只是一个人在扮演不同的角色…… 科学家沉默了很久才公布这一条：宇宙里自始至终只有一个灵魂。它在时间里反复折返，出演每一个出生过的人。你是你，也是你妈，也是你小学同桌，也是那个在网上骂你的人。所谓\u0026quot;他人\u0026rdquo;，是同一个演员换了戏服，隔着时差，跟自己对戏。\n后果1（吵不动了） 一切争吵在物理层面失去意义。你骂的是自己，你卷的是自己，你插队挤开的还是自己。战争成了世界上最荒诞的行为艺术：一个人，分饰两军，隔着战壕朝自己开炮。军队解散那天没有仪式，因为实在不知道该向谁投降。\n后果2（爱变得很奇怪） 有人崩溃了：那我爱我妻子，算不算终极自恋？情书还怎么写——\u0026ldquo;致我自己的另一场演出\u0026rdquo;？但也有人想通了：正因为对面是你，爱才第一次有了保底。你永远不会真正失去任何人，散场的演员会在另一场戏里换个脸回来，还是你。\n后果3（终极共情，或终极孤独） 白天，人们对彼此前所未有地温柔——伤害任何人都是自残，帮助任何人都是自救，\u0026ldquo;己所不欲勿施于人\u0026quot;从劝诫变成了同义反复。夜里，同一批人失眠：如果所有人都是我，那这个宇宙里，其实只有一名观众。掌声是我的，嘘声是我的，连\u0026quot;孤独\u0026quot;这个词，也是我发明出来说给自己听的。\n答案6（番外）：论文无法复现 宣布答案的第三天，另一组科学家发了篇预印本：《关于死后去向实验的复现失败报告》。\n全世界长舒一口气。\n原来人类需要的从来不是答案，而是那一点点\u0026quot;不知道\u0026rdquo;。不知道，才有宗教，才有哲学，才有半部文学史，才有清明烧下去的纸钱，才有\u0026quot;来世再见\u0026quot;这句既是狠话又是情话的话。\n死亡是人类地图上最后一块没被标注的荒野——我们一边怕它，一边偷偷指望它永远别被开发。\n所以真正的问题也许从来不是\u0026quot;我们死后去哪里\u0026rdquo;。\n而是：答案揭晓之后，我们还能不能好好活。\n灾厄和幸福大概都会来，按人头随机发放——\n就像重生一样。\n","permalink":"https://lv-blog.pages.dev/posts/thoughts/after-death/","summary":"\u003cp\u003e如果我们死后去哪里被科学家揭晓了，世界会发生什么？\u003c/p\u003e\n\u003cp\u003e知晓答案的我们会迎来灾厄还是幸福？\u003c/p\u003e\n\u003cp\u003e我能想到的几种答案：\u003c/p\u003e\n\u003ch3 id=\"答案1原来我们死后会进入一个等待队列会在这个星球上失去一切记忆地随机再次以人形重生\"\u003e答案1：原来我们死后会进入一个等待队列，会在这个星球上失去一切记忆地随机再次以人形重生\u003c/h3\u003e\n\u003cp\u003e按当前地球人口的增速来看，可能还没到\u0026quot;需要等\u0026quot;的那一步，我们一领便当，便当都没来得及吃，就从另一个母亲的肚子里哇哇落地了。\u003c/p\u003e\n\u003cp\u003e如果重生被证明无需任何代价，即，你刚没意识，就突然从另一个人肚子里醒来。那么这会迎来几个相当有可能的后果：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e后果1（火速重开版）\u003c/strong\u003e 长到知道什么是财富差距的年龄，发现自己家里一贫如洗，赶紧找把枪把自己枪毙。人生变成一场无限次数的抽卡游戏，而\u0026quot;重开\u0026quot;这个梗第一次拥有了字面意义。各国紧急立法管制一切危险物品，但拦不住——你没法用\u0026quot;珍惜生命\u0026quot;去劝一个知道生命是复读机的人。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e后果2（恶人的崛起）\u003c/strong\u003e 反正人一辈子就是一死一循环的事，赶紧把坏事做尽，死比坐牢强。刑法的威慑力一夜清零：无期徒刑？我下一世照样是个白白净净的婴儿。死刑甚至成了越狱手段。人类引以为傲的整套惩罚体系，原来押的全是\u0026quot;人只活一次\u0026quot;这一个注。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e后果3（世界和平共同富裕）\u003c/strong\u003e 原来我们无论如何都要重返这个世上，我们应该互帮互助，每个人都来得开心去得开心，没有战争，没有争抢，因为不必，我们只需开心地永恒着。你今天往河里倒的垃圾，五十年后会流进你下一世的水杯；你今天投下的炸弹，炸的可能是你自己明天的产房。环保和和平不再需要道德说教——它们成了纯粹的、自私的、无可辩驳的利己行为。\u003c/p\u003e\n\u003cp\u003e这三种后果会同时发生。人类大概要用两百年惨烈地筛选，最后活下来的文明版本，多半是后果3。不是因为人性本善，而是因为前两种玩法的玩家，会一遍一遍重生在被自己搞烂的世界里，直到玩明白为止。\u003c/p\u003e\n\u003ch3 id=\"答案2还是随机重生但是谁跟你说你还是人的形态了\"\u003e答案2：还是随机重生，但是谁跟你说你还是人的形态了？\u003c/h3\u003e\n\u003cp\u003e重生池是全物种共享的。按生物量算一算就会脊背发凉：地球上全人类不到六亿吨，蚂蚁跟我们差不多重，线虫、浮游生物、细菌加起来是我们的几千倍。也就是说，在轮回抽卡池里，\u0026ldquo;人类\u0026quot;是个出货率低到离谱的SSR——你这一世能坐在这里刷手机，已经是欧皇本皇了。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e后果1（全民吃素版）\u003c/strong\u003e 牛肉面馆一夜倒闭，因为那碗牛肉有万分之一的概率是你上一世的班主任。拍死一只蚊子要不要按\u0026quot;过失\u0026quot;处理成了立法难题，夏天变得极其难熬。渔业畜牧业连夜转型，人类食谱倒退回采集时代——好在植物暂时还没被查出在池子里。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e后果2（体验派的狂欢）\u003c/strong\u003e 也有人开始期待死亡：\u0026ldquo;下辈子想抽个信天翁，环游世界不用签证。\u0026ldquo;\u0026ldquo;我想当一棵红杉，站两千年，看你们轮回。\u0026ldquo;殡仪馆的悼词从\u0026quot;一路走好\u0026quot;变成\u0026quot;祝你出货\u0026rdquo;。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e后果3（人类中心主义崩塌）\u003c/strong\u003e \u0026ldquo;众生平等\u0026quot;从宗教口号变成物理事实。动物园关门，实验室白鼠获得赔偿性放生。人类第一次意识到：我们不是地球的主角，只是暂时借了一副带拇指的身体。\u003c/p\u003e\n\u003ch3 id=\"答案3还是随机重生但是谁跟你说是在地球了\"\u003e答案3：还是随机重生，但是谁跟你说是在地球了？\u003c/h3\u003e\n\u003cp\u003e重生范围：全宇宙。可观测宇宙少说两万亿个星系，你重生回地球的概率，约等于连续一百次中彩票头奖。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e后果1（天文学成为殡葬行业）\u003c/strong\u003e 天文台门口排起长队，家属握着天文学家的手：\u0026ldquo;您看我妈她……大概往哪个方向去了？\u0026ldquo;墓碑不再朝南，改朝室女座超星系团。清明烧纸的时候顺便烧一台射电望远镜模型，聊胜于无。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e后果2（宇宙级躺平）\u003c/strong\u003e 大家突然不焦虑了。房贷？内卷？KPI？下辈子我可能是猎户座旋臂上一坨快乐的星际气体，在零下两百七十度里舒展十亿年。这辈子这点破事，格局小了。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e后果3（宇宙级恐慌）\u003c/strong\u003e 也有人算了另一笔账：宇宙的绝大部分是真空、辐射和孤独。重生成一颗在星系间漂流一百亿年的氢原子——这到底算重生，还是无期徒刑？哲学系连夜开题：《论\u0026quot;存在\u0026quot;是否必须以\u0026quot;体验\u0026quot;为前提》。答辩没人通过，因为没人敢下结论。\u003c/p\u003e\n\u003ch3 id=\"答案4完了发现原来我们一生做的好事坏事真的有积分系统计算着\"\u003e答案4：完了，发现原来我们一生做的好事坏事真的有积分系统计算着\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e后果1（功德通货膨胀）\u003c/strong\u003e 斑马线前扶老奶奶的队伍排出两公里，老奶奶成为稀缺资源，出现职业\u0026quot;被扶员\u0026rdquo;，时薪不菲。公交车让座要靠抢，献血站限流，捐款平台崩溃。善良第一次出现了产能过剩。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e后果2（动机审查悖论）\u003c/strong\u003e 科学家紧急补充说明：系统会识别动机，刷分无效。全人类当场哀嚎——那我现在做好事，到底是出于善良，还是出于\u0026quot;想让系统觉得我出于善良\u0026rdquo;，还是\u0026quot;知道系统会审查动机所以努力让自己真诚\u0026quot;的三阶套娃？这成为新时代哲学第一难题，康德全网售罄。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e后果3（历史清算）\u003c/strong\u003e 有人翻出自己一生的流水账，发现小学偷拿同桌一块橡皮被扣了2分，至今没涨回来，当场崩溃。也有人发现自己随手做的一件小事——雨天给流浪猫挪了个纸箱——加的分比捐一栋楼还多。人们终于摸清了系统的口味：它不看金额，看你当时以为没人看见。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e后果4（宗教界的复杂心情）\u003c/strong\u003e 各大宗教一半在庆祝\u0026quot;早说过了\u0026rdquo;，一半在紧急对账——教义里的计分规则和实测数据对不上，误差还不小。神学院连夜改开数据分析课。\u003c/p\u003e\n\u003ch3 id=\"答案5完了原来所有的你你我我最后只是一个人在扮演不同的角色\"\u003e答案5：完了，原来所有的你你我我，最后只是一个人在扮演不同的角色……\u003c/h3\u003e\n\u003cp\u003e科学家沉默了很久才公布这一条：宇宙里自始至终只有一个灵魂。它在时间里反复折返，出演每一个出生过的人。你是你，也是你妈，也是你小学同桌，也是那个在网上骂你的人。所谓\u0026quot;他人\u0026rdquo;，是同一个演员换了戏服，隔着时差，跟自己对戏。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e后果1（吵不动了）\u003c/strong\u003e 一切争吵在物理层面失去意义。你骂的是自己，你卷的是自己，你插队挤开的还是自己。战争成了世界上最荒诞的行为艺术：一个人，分饰两军，隔着战壕朝自己开炮。军队解散那天没有仪式，因为实在不知道该向谁投降。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e后果2（爱变得很奇怪）\u003c/strong\u003e 有人崩溃了：那我爱我妻子，算不算终极自恋？情书还怎么写——\u0026ldquo;致我自己的另一场演出\u0026rdquo;？但也有人想通了：正因为对面是你，爱才第一次有了保底。你永远不会真正失去任何人，散场的演员会在另一场戏里换个脸回来，还是你。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e后果3（终极共情，或终极孤独）\u003c/strong\u003e 白天，人们对彼此前所未有地温柔——伤害任何人都是自残，帮助任何人都是自救，\u0026ldquo;己所不欲勿施于人\u0026quot;从劝诫变成了同义反复。夜里，同一批人失眠：如果所有人都是我，那这个宇宙里，其实只有一名观众。掌声是我的，嘘声是我的，连\u0026quot;孤独\u0026quot;这个词，也是我发明出来说给自己听的。\u003c/p\u003e\n\u003ch3 id=\"答案6番外论文无法复现\"\u003e答案6（番外）：论文无法复现\u003c/h3\u003e\n\u003cp\u003e宣布答案的第三天，另一组科学家发了篇预印本：《关于死后去向实验的复现失败报告》。\u003c/p\u003e\n\u003cp\u003e全世界长舒一口气。\u003c/p\u003e\n\u003cp\u003e原来人类需要的从来不是答案，而是那一点点\u0026quot;不知道\u0026rdquo;。不知道，才有宗教，才有哲学，才有半部文学史，才有清明烧下去的纸钱，才有\u0026quot;来世再见\u0026quot;这句既是狠话又是情话的话。\u003c/p\u003e\n\u003cp\u003e死亡是人类地图上最后一块没被标注的荒野——我们一边怕它，一边偷偷指望它永远别被开发。\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e所以真正的问题也许从来不是\u0026quot;我们死后去哪里\u0026rdquo;。\u003c/p\u003e\n\u003cp\u003e而是：答案揭晓之后，我们还能不能好好活。\u003c/p\u003e\n\u003cp\u003e灾厄和幸福大概都会来，按人头随机发放——\u003c/p\u003e\n\u003cp\u003e就像重生一样。\u003c/p\u003e","title":"如果我们死后去哪里被科学家揭晓了，世界会发生什么？知晓答案的我们会迎来灾厄还是幸福？"},{"content":"偏移量索引 高可用 ISR机制 ","permalink":"https://lv-blog.pages.dev/posts/programming/backend/kafka-%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/","summary":"\u003ch2 id=\"偏移量索引\"\u003e偏移量索引\u003c/h2\u003e\n\u003ch2 id=\"高可用\"\u003e高可用\u003c/h2\u003e\n\u003ch2 id=\"isr机制\"\u003eISR机制\u003c/h2\u003e","title":"Kafka 学习笔记"},{"content":"一、读路径：Cache Aside(旁路缓存) 先查 Redis，命中就返回；未命中查 DB，回写 Redis 后返回。\nRedis 三件套\n问题 场景 解法 穿透 查一个不存在的 key，每次都打到 DB 缓存空值(短 TTL) + 布隆过滤器 + 参数校验 击穿 某个热点 key 过期，瞬间大量请求打到 DB 分布式锁只放一个线程回源 + 逻辑过期(异步重建) + 热点 key 不过期 雪崩 大批 key 同时过期或 Redis 宕机 TTL 加随机抖动 + 多级缓存(Caffeine + Redis) + 熔断降级(Sentinel) + 集群/哨兵高可用 二、写路径：先写 DB，再删缓存 不用\u0026quot;更新缓存\u0026quot;而用\u0026quot;删除缓存\u0026quot;：避免并发写导致的脏数据，也避免为不会被读的数据做无用计算(lazy loading)。\n为什么是\u0026quot;先 DB 后删\u0026quot;而不是\u0026quot;先删后写 DB\u0026quot;？ 先删缓存，在写 DB 的间隙可能有读请求把旧值重新加载进缓存，且这个旧值会一直存在直到过期。\n但先写后删仍有极小概率不一致(读线程读到旧 DB 值 → 写线程写 DB + 删缓存 → 读线程回写旧值)。兜底方案：\n缓存本身设 TTL(最终一致) 延迟双删：删缓存 → 写 DB → sleep 一小段 → 再删一次 Canal 订阅 binlog 异步删缓存(与业务解耦，最可靠) 三、写 DB + 发消息：本地消息表(Outbox) 不能\u0026quot;写完 DB 再发 MQ\u0026quot;——两者不在同一事务里，发消息失败或应用宕机就丢消息；也不能先发 MQ，DB 可能回滚。\nOutbox 做法：把消息当成一条记录，和业务数据在同一个本地事务里写进同一个库。事务提交 = 消息一定不丢。再由独立的投递方(定时轮询 outbox 表 / Canal 订阅 binlog)读出来发 MQ，发成功后标记已发送。\n代价：至少投递一次(at-least-once)，消费端必须幂等(唯一约束 / 去重表 / 状态机判断)。\n横向扩容注意： 分布式锁。同一时刻只能有一个实例进行轮询\ngraph TD A[业务请求] --\u003e B[开启本地事务] B --\u003e C[写业务表] C --\u003e D[写 outbox 表] D --\u003e E[提交事务] E --\u003e F[投递方轮询/binlog] F --\u003e G[发送到 Kafka] G --\u003e H[标记已发送] H --\u003e I[消费端幂等处理] 四、分布式锁 用途：跨 JVM 的互斥。缓存击穿回源、防重复下单、定时任务单实例执行。\nRedis 实现要点\nSET key value NX EX \u0026lt;ttl\u0026gt; 一条命令完成加锁+设过期，防死锁 value 存唯一标识(UUID)，解锁必须用 Lua 脚本校验 value 再删，防止删掉别人的锁 锁过期但业务没执行完 → 看门狗自动续期(Redisson tryLock 默认 30s，每 10s 续期) 生产直接用 Redisson，别手写 选型对比\n方案 一致性 性能 说明 Redis (Redisson) 弱(主从切换可能丢锁) 高 绝大多数场景够用 RedLock 较强 中 争议大，Redisson 有实现 ZooKeeper / etcd 强(CP) 中低 临时顺序节点 + Watch，无羊群效应 DB 唯一索引/悲观锁 强 低 简单场景可用 注意：分布式锁只是性能优化和降低冲突概率，不能替代数据库层面的最终一致性保证(唯一约束、乐观锁版本号)。\n五、消息队列相关 如何保证消息不丢？三段式\n阶段 措施 生产端 Kafka acks=all + retries + 回调确认；RocketMQ 事务消息 / Outbox Broker 端 多副本(replication.factor\u0026gt;=3)、min.insync.replicas=2、关闭 unclean 选举、刷盘策略 消费端 关闭自动提交，业务处理成功后手动 commit offset 消息重复 / 幂等：网络重传 + at-least-once 决定了必然重复。解法：唯一业务 ID + 去重表 / Redis SETNX / 数据库唯一索引 / 状态机流转。\n消息顺序：Kafka 只保证单 Partition 有序。同一业务 key 路由到同一分区(如订单 ID 做 key)，消费端单线程或按 key 哈希到线程处理。\n消息积压：先看是消费慢还是生产暴增。临时扩分区+扩消费者、消费端改批量/异步、必要时先转存再补偿。\n死信队列：重试 N 次仍失败进 DLQ,人工/定时补偿,避免阻塞整个分区。\n六、分布式事务全景 graph TD A[分布式事务] --\u003e B[强一致] A --\u003e C[最终一致] B --\u003e B1[2PC/XA同步阻塞,性能差] B --\u003e B2[Seata AT自动生成反向SQL] C --\u003e C1[TCCTry-Confirm-Cancel侵入强,性能好] C --\u003e C2[Saga长事务,补偿操作] C --\u003e C3[本地消息表/Outbox最常用] C --\u003e C4[事务消息RocketMQ半消息] 面试话术：能不用分布式事务就不用(合并服务边界)。必须用时,金融强一致选 TCC,长流程选 Saga,跨系统通知选 Outbox。\n七、缓存进阶 多级缓存：Caffeine(本地) → Redis → DB。本地缓存需处理集群间一致性(MQ 广播失效) 热点 Key 探测：JD hotkey / 自研统计,发现后打散到本地缓存 大 Key / 热 Key 问题：大 key 阻塞主线程(hash 分片存储),热 key 打垮单节点(加随机后缀分散) 缓存预热：上线前/定时任务把热数据灌进 Redis Redis 持久化：RDB(快照,快但丢数据) + AOF(日志,everysec 折中),生产混合模式 八、数据库 索引：最左前缀、覆盖索引、索引下推、为什么用 B+ 树不用 B 树/哈希 MVCC：undo log 版本链 + ReadView,RC 每次读生成、RR 事务首次读生成 锁:记录锁/间隙锁/临键锁,RR 下如何避免幻读 三大日志：redo log(崩溃恢复,WAL)、undo log(回滚+MVCC)、binlog(主从+归档),两阶段提交保证一致 主从延迟：写后读走主库 / 半同步复制 / 并行复制 分库分表：ShardingSphere,分片键选择,跨库 join 和分页问题,扩容(一致性哈希/双写迁移) 九、高可用与限流 手段 说明 限流 固定窗口 / 滑动窗口 / 漏桶 / 令牌桶(Sentinel、Guava RateLimiter) 熔断 错误率或 RT 超阈值直接快速失败,半开状态探活(Sentinel/Resilience4j) 降级 非核心链路返回兜底数据 隔离 线程池隔离 / 信号量隔离,防止雪崩传导 超时重试 必须设超时;重试要配合幂等 + 退避,否则放大故障 十、幂等设计(单独考的高频点) 场景 方案 前端重复提交 Token 机制(下单前领 token,提交时 Redis 删除) 接口重复调用 唯一请求 ID + 去重表(唯一索引) 状态流转 UPDATE ... WHERE status = '待支付' 乐观锁 MQ 重复消费 消费记录表 / Redis SETNX 十一、ID 生成 雪花算法(Snowflake) 时钟回拨问题、美团 Leaf(号段模式 + Snowflake)、UUID 无序导致 B+ 树页分裂。\n十二、经典业务场景题 秒杀：前端限流 → 网关限流 → Redis 预扣库存(Lua 保证原子) → MQ 削峰异步下单 → 库存最终对账 延迟任务：Redis ZSet / RocketMQ 延迟消息 / 时间轮 / DB 轮询 排行榜：Redis ZSet 分布式 Session：Redis 集中存储 / JWT 无状态(你的 Sweet Home 就是这套) ","permalink":"https://lv-blog.pages.dev/posts/programming/backend/db-mq-redis%E5%B8%B8%E8%A7%81%E5%AE%9E%E8%B7%B5/","summary":"\u003ch3 id=\"一读路径cache-aside旁路缓存\"\u003e一、读路径：Cache Aside(旁路缓存)\u003c/h3\u003e\n\u003cp\u003e先查 Redis，命中就返回；未命中查 DB，回写 Redis 后返回。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eRedis 三件套\u003c/strong\u003e\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e问题\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e场景\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e解法\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e穿透\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e查一个\u003cstrong\u003e不存在\u003c/strong\u003e的 key，每次都打到 DB\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e缓存空值(短 TTL) + 布隆过滤器 + 参数校验\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e击穿\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e某个\u003cstrong\u003e热点 key 过期\u003c/strong\u003e，瞬间大量请求打到 DB\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e分布式锁只放一个线程回源 + 逻辑过期(异步重建) + 热点 key 不过期\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e雪崩\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e大批 key 同时过期\u003c/strong\u003e或 Redis 宕机\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eTTL 加随机抖动 + 多级缓存(Caffeine + Redis) + 熔断降级(Sentinel) + 集群/哨兵高可用\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"二写路径先写-db再删缓存\"\u003e二、写路径：先写 DB，再删缓存\u003c/h3\u003e\n\u003cp\u003e不用\u0026quot;更新缓存\u0026quot;而用\u0026quot;删除缓存\u0026quot;：避免并发写导致的脏数据，也避免为不会被读的数据做无用计算(lazy loading)。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e为什么是\u0026quot;先 DB 后删\u0026quot;而不是\u0026quot;先删后写 DB\u0026quot;？\u003c/strong\u003e\n先删缓存，在写 DB 的间隙可能有读请求把\u003cstrong\u003e旧值\u003c/strong\u003e重新加载进缓存，且这个旧值会一直存在直到过期。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e但先写后删仍有极小概率不一致\u003c/strong\u003e(读线程读到旧 DB 值 → 写线程写 DB + 删缓存 → 读线程回写旧值)。兜底方案：\u003c/p\u003e","title":"DB MQ Redis 常见实践"},{"content":"1. 核心大招：强制“增量输出”（Diff-Only） 这是节省 输出 Token 最有效的手段。默认情况下，大模型喜欢把整份代码文件重新写一遍，即便它只改了一行。\n笨办法：“帮我修改这个文件的 bug。”（模型直接吐出 500 行代码） 妙招：在 Prompt 中明确限制输出格式。 Prompt 模板： “请只输出修改过的代码片段。未修改的部分请用 // ... 保持不变 ... 或 # ... existing code ... 代替。如果可以，请直接以 Git Diff 的格式提供修改方案。”\n2. 输入“骨架”而非全量代码（Skeleton Prompting） 当你需要让模型理解你的项目结构或某个工具类时，不要直接把几千行的源文件贴过去。\n妙招：只给模型提供类型定义（Types/Interfaces）、函数签名（Function Signatures） 或 骨架代码。 例如（TS/JS）： // 不要贴整个实现，只给这个： interface UserService { getUser(id: string): Promise\u0026lt;User\u0026gt;; updateProfile(id: string, data: Partial\u0026lt;User\u0026gt;): Promise\u0026lt;boolean\u0026gt;; } 模型只需要知道“有什么方法可用”以及“入参和出参是什么”，并不需要知道你内部复杂的 SQL 是怎么写的。\n3. 巧用 XML 标签（对 Claude 尤为有效） Claude 对 XML 标签（如 \u0026lt;code_\u0026gt;）有着极强的敏感度。用 XML 标签包裹代码和指令，不仅能提高准确率，还能减少解释性废话。\n\u0026lt;system_instruction\u0026gt; 你是一个极简主义的编程助手。请直接输出代码，不要解释，不要说“好的，我为你准备了以下代码”。 \u0026lt;/system_instruction\u0026gt; \u0026lt;source_code\u0026gt; // 贴入你的核心代码 \u0026lt;/source_code\u0026gt; \u0026lt;task\u0026gt; 重构上面的代码，提高运行效率。 \u0026lt;/task\u0026gt; 省流原理：明确的结构让模型不需要在 Prompt 里猜测“哪里是代码，哪里是要求”，从而大大减少了模型的推理和多余的客套话（\u0026ldquo;Certainly! Here is\u0026hellip;\u0026rdquo; 也是要算 Token 的）。\n4. 及时“断舍离”，开启新会话（Reset Context） 很多人习惯在一个聊天窗口里从早聊到晚，甚至聊上好几天。\n痛点：每一次你发送新消息，大模型都会把之前所有的聊天记录重新打包发送一遍（这叫 Cumulative Token）。如果前文有 5 个版本的完整代码，你发一句“谢谢，搞定了”，这一句谢谢可能要消耗你几万 Token。 妙招：一个 Bug 解决后，立刻开新窗口。 开启前，可以把上一个窗口最终确定的方案或关键 Context 复制过去作为新会话的起点。 5. 清理输入代码的“水分” 在把代码贴给 LLM 之前，做一些简单的预处理：\n删掉不必要的日志和注释（比如大段的 JSDoc/Docstring 或者是历史 debug 的 console.log）。 避免贴入无关静态资源：例如代码中嵌套的巨长 Base64 字符串、大型 SVG 矢量图代码、几百行的 mock 数据。贴入前把它们替换成 const logoSvg = \u0026quot;...\u0026quot; // SVG Content here。 ⚖️ 省流策略对比表 场景 以前的做法（Token 刺客） 现在的做法（Token 守财奴） 预计节省率 修改已有代码 直接贴全文件，让它重新生成全文件 使用 // ... 保持不变 ... 指令，只输出修改点 50% - 80% 让模型理解多文件关系 把相关的文件一股脑全贴过去 只贴相关文件的目录树（tree）和对外接口定义 70% - 90% 多轮 Debug 在同一个长对话里反复调试 Debug 成功后，立刻复制正确代码，开新窗口继续下一步 30% - 60%（越往后省越多） ","permalink":"https://lv-blog.pages.dev/posts/programming/some-tricks/%E5%A6%82%E4%BD%95%E5%B0%BD%E5%8F%AF%E8%83%BD%E5%87%8F%E5%B0%91ai-coding%E4%BD%BF%E7%94%A8%E7%9A%84token/","summary":"\u003ch3 id=\"1-核心大招强制增量输出diff-only\"\u003e1. 核心大招：强制“增量输出”（Diff-Only）\u003c/h3\u003e\n\u003cp\u003e这是节省 \u003cstrong\u003e输出 Token\u003c/strong\u003e 最有效的手段。默认情况下，大模型喜欢把整份代码文件重新写一遍，即便它只改了一行。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e笨办法\u003c/strong\u003e：“帮我修改这个文件的 bug。”（模型直接吐出 500 行代码）\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e妙招\u003c/strong\u003e：在 Prompt 中明确限制输出格式。\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003ePrompt 模板：\u003c/strong\u003e\n“请只输出修改过的代码片段。未修改的部分请用 \u003ccode\u003e// ... 保持不变 ...\u003c/code\u003e 或 \u003ccode\u003e# ... existing code ...\u003c/code\u003e 代替。如果可以，请直接以 Git Diff 的格式提供修改方案。”\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch3 id=\"2-输入骨架而非全量代码skeleton-prompting\"\u003e2. 输入“骨架”而非全量代码（Skeleton Prompting）\u003c/h3\u003e\n\u003cp\u003e当你需要让模型理解你的项目结构或某个工具类时，\u003cstrong\u003e不要\u003c/strong\u003e直接把几千行的源文件贴过去。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e妙招\u003c/strong\u003e：只给模型提供\u003cstrong\u003e类型定义（Types/Interfaces）\u003c/strong\u003e、\u003cstrong\u003e函数签名（Function Signatures）\u003c/strong\u003e 或 \u003cstrong\u003e骨架代码\u003c/strong\u003e。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e例如（TS/JS）\u003c/strong\u003e：\u003c/li\u003e\n\u003c/ul\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-typescript\" data-lang=\"typescript\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 不要贴整个实现，只给这个：\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003einterface\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eUserService\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#a6e22e\"\u003egetUser\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003eid\u003c/span\u003e: \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e)\u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ePromise\u003c/span\u003e\u0026lt;\u003cspan style=\"color:#f92672\"\u003eUser\u003c/span\u003e\u0026gt;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#a6e22e\"\u003eupdateProfile\u003c/span\u003e(\u003cspan style=\"color:#a6e22e\"\u003eid\u003c/span\u003e: \u003cspan style=\"color:#66d9ef\"\u003estring\u003c/span\u003e, \u003cspan style=\"color:#a6e22e\"\u003edata\u003c/span\u003e: \u003cspan style=\"color:#66d9ef\"\u003ePartial\u003c/span\u003e\u0026lt;\u003cspan style=\"color:#f92672\"\u003eUser\u003c/span\u003e\u0026gt;)\u003cspan style=\"color:#f92672\"\u003e:\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ePromise\u003c/span\u003e\u0026lt;\u003cspan style=\"color:#f92672\"\u003eboolean\u003c/span\u003e\u0026gt;;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e模型只需要知道“有什么方法可用”以及“入参和出参是什么”，并不需要知道你内部复杂的 SQL 是怎么写的。\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"3-巧用-xml-标签对-claude-尤为有效\"\u003e3. 巧用 XML 标签（对 Claude 尤为有效）\u003c/h3\u003e\n\u003cp\u003eClaude 对 XML 标签（如 \u003ccode\u003e\u0026lt;code_\u0026gt;\u003c/code\u003e）有着极强的敏感度。用 XML 标签包裹代码和指令，不仅能提高准确率，还能\u003cstrong\u003e减少解释性废话\u003c/strong\u003e。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;system_instruction\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e你是一个极简主义的编程助手。请直接输出代码，不要解释，不要说“好的，我为你准备了以下代码”。\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/system_instruction\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;source_code\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e// 贴入你的核心代码\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/source_code\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;task\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e重构上面的代码，提高运行效率。\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/task\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e省流原理\u003c/strong\u003e：明确的结构让模型不需要在 Prompt 里猜测“哪里是代码，哪里是要求”，从而大大减少了模型的推理和多余的客套话（\u0026ldquo;Certainly! Here is\u0026hellip;\u0026rdquo; 也是要算 Token 的）。\u003c/p\u003e","title":"AI Coding 省流指南"},{"content":" 从互联网整理而来\n一、Dart 基础 Dart 的数据类型有哪些？ var、final、const 的区别 Dart 中 null safety 是什么，如何使用？ late 关键字作用与使用场景 Dart 函数可选参数、命名参数、位置参数区别 箭头函数适用范围 Dart 异步：Future、async/await 原理 Stream 与 Future 区别，StreamController 使用 sync*、async*、yield、yield each 作用 Dart 类中 extends、implements、mixin 区别 mixin 有什么限制，能否构造函数传参 抽象类与接口区别 泛型 T、?T、T?、required T 区别 Dart 单例几种实现方式 工厂构造函数 factory 作用 常量构造函数使用条件 级联运算符 .. 原理 空值操作符 ?.、??、??= 用法 Dart 枚举进阶用法，能否自定义属性方法 Isolate 是什么，和线程区别，通信方式 Isolate 共享内存吗，数据传递限制 Zone 作用是什么 Dart 垃圾回收机制 函数一等公民体现在哪里 typedef 作用与场景 二、Flutter 框架核心原理 Flutter 三棵树：Widget、Element、RenderTree 关系 StatelessWidget 和 StatefulWidget 底层差异 State 生命周期完整流程 initState、didChangeDependencies、build、didUpdateWidget、dispose 执行时机 setState 底层更新机制 Widget 为什么是不可变的 immutable Element 作用，Element 复用逻辑 RenderObject 职责是什么 Key 的作用，几种 Key 区别（ValueKey、ObjectKey、UniqueKey、GlobalKey） GlobalKey 使用场景与隐患 LocalKey 解决什么问题 Widget 重建流程，哪些情况会触发重建 什么是 const Widget，好处是什么 Flutter 渲染流程：布局、绘制、合成步骤 布局约束流程：Constraints 传递规则 父布局约束如何限制子控件尺寸 RepaintBoundary 作用，何时使用 Layer 树作用，缓存机制 Flutter 与原生 Android/iOS 渲染区别 Skia 引擎作用 Impeller 渲染引擎对比 Skia 优势 Flutter 绘制原理，Canvas、Paint 流程 PipelineOwner 作用 WidgetsBinding、SchedulerBinding 职责 帧调度流程：Vsync、帧回调执行顺序 transientCallbacks、persistentCallbacks、postFrameCallbacks 区别 Flutter 热重载原理，热重启区别 编译模式：debug/profile/release 差异 AOT、JIT 编译区别 Flutter 页面路由底层实现原理 三、布局 \u0026amp; 基础组件 Row Column Flex 布局区别，主轴交叉轴概念 MainAxisAlignment、CrossAxisAlignment 所有枚举值含义 Expanded、Flexible 区别，flex 参数作用 Stack、Positioned 布局规则 Align、Center 底层关系 Padding、Container 内部结构 Container 嵌套过多有什么性能问题 SizedBox、ConstrainedBox、UnconstrainedBox 用途 AspectRatio、FittedBox 缩放逻辑 Wrap 自动换行实现原理 ListView 几种构造方式区别（默认、builder、separated） ListView.builder 高性能原理，复用机制 ListView 滚动复用失效场景 GridView 使用与性能优化 SingleChildScrollView 嵌套 ListView 报错解决方案 CustomScrollView、Sliver 体系是什么 SliverList、SliverGrid、SliverAppBar、SliverToBoxAdapter 使用场景 SliverPersistentHeader 固定吸顶实现 CustomScrollView 滚动统一原理 Table、DataTable 布局特性 IntrinsicHeight、IntrinsicWidth 性能损耗原因 LayoutBuilder 获取父布局约束场景 MediaQuery 获取设备信息，MediaQuery.of(context) 刷新机制 LayoutId、MultiChildLayout 自定义布局 Flow 高性能流式布局适用场景 四、状态管理 State 自身状态管理优缺点 InheritedWidget 底层原理，数据传递逻辑 InheritedModel 对比 InheritedWidget 优势 Provider 整套库实现原理（ChangeNotifierProvider、Consumer、Selector） Selector 为什么能精准局部刷新 ChangeNotifier 通知更新机制 StateProvider、FutureProvider、StreamProvider 区别 Riverpod 对比 Provider 改进点 Riverpod 全局状态、自动 dispose、无 context 优势 StateNotifier、AsyncNotifier 使用场景 Bloc 核心概念：Event、State、Bloc/Cubit Cubit 与 Bloc 区别 BlocProvider、BlocConsumer、BlocBuilder、BlocListener 分工 Bloc 状态单一数据源优势 GetX 状态管理原理，GetController 生命周期 GetX 全局路由、依赖注入实现 GetX 优缺点，存在什么性能隐患 Redux 在 Flutter 中使用逻辑 MobX 响应式原理，Observable、Action、Reaction 大型项目如何选型状态管理方案 跨页面共享状态几种实现方式对比 状态持久化配合状态管理怎么做 五、路由 \u0026amp; 导航 Navigator 1.0 路由栈管理原理 MaterialPageRoute、CupertinoPageRoute 区别 PageRouteBuilder 自定义转场动画 Navigator push/pop/pushReplacement/popUntil 逻辑 路由传参几种方式 Navigator 2.0 核心组件：Router、RouteInformationParser、RouterDelegate Navigator 2.0 完整路由流程 GoRouter 框架底层封装逻辑 GoRouter 嵌套路由、路由守卫、参数获取 路由栈持久化、页面状态保存方案 底部导航多 Tab 页面状态保持实现 路由拦截、登录鉴权统一处理 原生页面与 Flutter 页面互相跳转路由处理 页面返回监听 WillPopScope、PopScope 深度链接 deep link 适配方案 六、动画 Flutter 动画核心类：Animation、AnimationController、CurvedAnimation AnimationController vsync 作用，为什么需要 TickerProvider SingleTickerProviderStateMixin、TickerProviderStateMixin 区别 Tween、Animatable 差值计算原理 显式动画与隐式动画区别 AnimatedContainer、AnimatedOpacity 等隐式动画底层 Hero 共享元素动画实现原理 PageRoute 共享 Hero 流程 CustomAnimation 自定义动画组件 交错动画 Interval 使用 动画性能优化，避免重建 Lottie 动画集成原理，性能注意点 循环动画、反向动画实现 动画暂停、恢复、释放资源处理 自定义路由转场动画 七、网络 \u0026amp; 本地存储 dio 库封装思路，拦截器使用 dio 统一请求头、错误处理、token 自动刷新 取消请求 CancelToken 使用场景 并发请求、超时处理 Json 序列化反序列化几种方式（手动、json_serializable） fromJson/toJson 生成流程 Freezed 数据模型优势 http 库与 dio 对比 WebSocket 在 Flutter 中使用，重连机制 shared_preferences 底层存储原理 Hive 本地数据库优势，Box 使用 Hive 加密、大数据存储优化 sqflite 数据库操作，事务使用 Isolate 中操作数据库注意事项 文件读写 path_provider 获取路径 图片缓存 cached_network_image 实现逻辑 网络请求缓存策略设计 接口统一异常捕获封装方案 八、原生交互 MethodChannel MethodChannel、BasicMessageChannel、EventChannel 三者区别 MethodChannel 双向通信完整流程 Flutter 调用 Android 原生代码步骤 Flutter 调用 iOS OC/Swift 步骤 原生主动推送数据给 Flutter 使用哪个 Channel 传参类型限制，复杂对象传递方案 多页面 Channel 单例管理 PlatformView 嵌入原生 View（Android View、iOS UIView） PlatformView 性能问题与优化 原生页面跳转 Flutter 传参 权限通过原生通道申请方案 混合栈开发：flutterBoost 原理 flutterBoost 页面生命周期同步问题 原生资源与 Flutter 资源冲突处理 通信通道内存泄漏如何避免 九、性能优化专项 Widget 重建常见诱因与规避手段 const 构造优化原理 Key 不合理使用导致的性能问题 ListView 长列表优化方案 图片加载优化：尺寸、缓存、压缩 RepaintBoundary 拆分重绘区域场景 减少 Layer 树层级优化 避免使用 Intrinsic 系列控件原因 动画卡顿排查思路 DevTools 工具使用：性能面板、内存、Widget 检查 内存泄漏常见场景（Timer、Stream、Channel、Animation） 大图片、大量对象内存释放方案 Isolate 处理耗时计算优化 UI build 方法避免耗时操作 布局嵌套过深优化手段 页面 dispose 资源释放规范 Profile 模式性能排查要点 启动速度优化（包体积、懒加载、预加载） Flutter 包体积优化方案 滑动卡顿完整排查流程 十、图片、手势、输入、弹窗组件 Image 几种构造：AssetImage、NetworkImage、FileImage、MemoryImage ImageProvider 缓存机制 Image.network 缓存缺陷，cached_network_image 弥补方案 DecorationImage、FadeInImage 使用 GestureDetector 手势识别原理，多手势冲突 Listener 底层原始手势与 GestureDetector 区别 手势竞争、手势忽略场景 InkWell 水波纹实现原理，必须嵌套 Material TextField 控制器 TextEditingController 使用 TextField 输入监听、格式化、光标控制 表单 Form、GlobalKey 校验逻辑 ModalRoute、showDialog、showBottomSheet 弹窗原理 弹窗状态共享问题解决方案 Draggable、LongPressDraggable 拖拽实现 Transform 变换不改变布局约束特性 十一、项目工程 \u0026amp; 打包适配 pubspec.yaml 各字段含义，依赖版本约束 本地依赖、Git 依赖、远程依赖配置 pub 依赖冲突解决方法 Flutter 多环境配置（开发/测试/生产） Flavor 多渠道打包 Android \u0026amp; iOS Android 打包 keystore 配置，iOS 证书配置 AndroidManifest、Info.plist 权限适配 屏幕适配方案：媒体查询、比例、flutter_screenutil 深色模式适配实现 多语言国际化 intl 库使用 App 图标、启动页配置 Flutter 插件开发流程 平台区分代码编写（dart.io、Platform 判断） 混合开发项目目录结构 CI/CD 自动打包脚本思路 版本号管理升级方案 第三方插件兼容问题处理 十二、进阶底层 \u0026amp; 疑难场景 WidgetsApp、MaterialApp、CupertinoApp 内部结构 ThemeData 主题分发原理，局部主题覆盖 Navigator 上下文与页面上下文区别 context 获取上层 Widget 原理，BuildContext 分类 State 保存在 Element 中，Widget 重建 State 不销毁条件 Offstage、Visibility 控件隐藏区别 AbsorbPointer、IgnorePointer 拦截手势差异 Clip 裁剪控件：ClipRect、ClipRRect、ClipOval 性能 BackdropFilter 模糊控件性能损耗原因 RenderBox 自定义控件完整流程 自定义 RenderObject 实现步骤 自定义 MultiChildRenderObjectWidget ScrollController 监听滚动、滚动定位 ScrollPhysics 滚动回弹、阻尼自定义 监听应用前后台切换 AppLifecycleState 截屏、屏幕亮度、震动原生能力实现 WebView 插件使用与通信 Flutter Web 编译差异，平台兼容问题 Flutter Windows/macOS/Linux 桌面端适配要点 如何实现全局捕获异常 Timer、StreamSubscription 忘记取消的内存泄漏 列表 item 点击状态保存方案 异步并发竞态条件问题处理 图片大量加载 OOM 解决方案 原生弹窗遮挡 Flutter 页面问题 十三、综合项目面试问答题 讲一下你项目整体架构分层 项目状态管理选型原因 长列表卡顿怎么优化的 多页面共享登录状态如何实现 token 过期统一拦截处理方案 项目混合栈开发遇到哪些坑 图片缓存、网络缓存怎么设计 全局网络加载弹窗、错误页封装思路 项目通用业务组件封装思路 线上卡顿、崩溃排查处理经验 如何实现页面无网络重试逻辑 复杂表单页面状态管理方案 项目包体积优化做了哪些操作 多渠道、多环境配置实现 封装通用 Dio 请求库完整思路 ","permalink":"https://lv-blog.pages.dev/posts/programming/frontend/flutter-interview-questions/","summary":"\u003cblockquote\u003e\n\u003cp\u003e从互联网整理而来\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"一dart-基础\"\u003e一、Dart 基础\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eDart 的数据类型有哪些？\u003c/li\u003e\n\u003cli\u003evar、final、const 的区别\u003c/li\u003e\n\u003cli\u003eDart 中 null safety 是什么，如何使用？\u003c/li\u003e\n\u003cli\u003elate 关键字作用与使用场景\u003c/li\u003e\n\u003cli\u003eDart 函数可选参数、命名参数、位置参数区别\u003c/li\u003e\n\u003cli\u003e箭头函数适用范围\u003c/li\u003e\n\u003cli\u003eDart 异步：Future、async/await 原理\u003c/li\u003e\n\u003cli\u003eStream 与 Future 区别，StreamController 使用\u003c/li\u003e\n\u003cli\u003esync*、async*、yield、yield each 作用\u003c/li\u003e\n\u003cli\u003eDart 类中 extends、implements、mixin 区别\u003c/li\u003e\n\u003cli\u003emixin 有什么限制，能否构造函数传参\u003c/li\u003e\n\u003cli\u003e抽象类与接口区别\u003c/li\u003e\n\u003cli\u003e泛型 T、?T、T?、required T 区别\u003c/li\u003e\n\u003cli\u003eDart 单例几种实现方式\u003c/li\u003e\n\u003cli\u003e工厂构造函数 factory 作用\u003c/li\u003e\n\u003cli\u003e常量构造函数使用条件\u003c/li\u003e\n\u003cli\u003e级联运算符 .. 原理\u003c/li\u003e\n\u003cli\u003e空值操作符 ?.、??、??= 用法\u003c/li\u003e\n\u003cli\u003eDart 枚举进阶用法，能否自定义属性方法\u003c/li\u003e\n\u003cli\u003eIsolate 是什么，和线程区别，通信方式\u003c/li\u003e\n\u003cli\u003eIsolate 共享内存吗，数据传递限制\u003c/li\u003e\n\u003cli\u003eZone 作用是什么\u003c/li\u003e\n\u003cli\u003eDart 垃圾回收机制\u003c/li\u003e\n\u003cli\u003e函数一等公民体现在哪里\u003c/li\u003e\n\u003cli\u003etypedef 作用与场景\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"二flutter-框架核心原理\"\u003e二、Flutter 框架核心原理\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eFlutter 三棵树：Widget、Element、RenderTree 关系\u003c/li\u003e\n\u003cli\u003eStatelessWidget 和 StatefulWidget 底层差异\u003c/li\u003e\n\u003cli\u003eState 生命周期完整流程\u003c/li\u003e\n\u003cli\u003einitState、didChangeDependencies、build、didUpdateWidget、dispose 执行时机\u003c/li\u003e\n\u003cli\u003esetState 底层更新机制\u003c/li\u003e\n\u003cli\u003eWidget 为什么是不可变的 immutable\u003c/li\u003e\n\u003cli\u003eElement 作用，Element 复用逻辑\u003c/li\u003e\n\u003cli\u003eRenderObject 职责是什么\u003c/li\u003e\n\u003cli\u003eKey 的作用，几种 Key 区别（ValueKey、ObjectKey、UniqueKey、GlobalKey）\u003c/li\u003e\n\u003cli\u003eGlobalKey 使用场景与隐患\u003c/li\u003e\n\u003cli\u003eLocalKey 解决什么问题\u003c/li\u003e\n\u003cli\u003eWidget 重建流程，哪些情况会触发重建\u003c/li\u003e\n\u003cli\u003e什么是 const Widget，好处是什么\u003c/li\u003e\n\u003cli\u003eFlutter 渲染流程：布局、绘制、合成步骤\u003c/li\u003e\n\u003cli\u003e布局约束流程：Constraints 传递规则\u003c/li\u003e\n\u003cli\u003e父布局约束如何限制子控件尺寸\u003c/li\u003e\n\u003cli\u003eRepaintBoundary 作用，何时使用\u003c/li\u003e\n\u003cli\u003eLayer 树作用，缓存机制\u003c/li\u003e\n\u003cli\u003eFlutter 与原生 Android/iOS 渲染区别\u003c/li\u003e\n\u003cli\u003eSkia 引擎作用\u003c/li\u003e\n\u003cli\u003eImpeller 渲染引擎对比 Skia 优势\u003c/li\u003e\n\u003cli\u003eFlutter 绘制原理，Canvas、Paint 流程\u003c/li\u003e\n\u003cli\u003ePipelineOwner 作用\u003c/li\u003e\n\u003cli\u003eWidgetsBinding、SchedulerBinding 职责\u003c/li\u003e\n\u003cli\u003e帧调度流程：Vsync、帧回调执行顺序\u003c/li\u003e\n\u003cli\u003etransientCallbacks、persistentCallbacks、postFrameCallbacks 区别\u003c/li\u003e\n\u003cli\u003eFlutter 热重载原理，热重启区别\u003c/li\u003e\n\u003cli\u003e编译模式：debug/profile/release 差异\u003c/li\u003e\n\u003cli\u003eAOT、JIT 编译区别\u003c/li\u003e\n\u003cli\u003eFlutter 页面路由底层实现原理\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"三布局--基础组件\"\u003e三、布局 \u0026amp; 基础组件\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eRow Column Flex 布局区别，主轴交叉轴概念\u003c/li\u003e\n\u003cli\u003eMainAxisAlignment、CrossAxisAlignment 所有枚举值含义\u003c/li\u003e\n\u003cli\u003eExpanded、Flexible 区别，flex 参数作用\u003c/li\u003e\n\u003cli\u003eStack、Positioned 布局规则\u003c/li\u003e\n\u003cli\u003eAlign、Center 底层关系\u003c/li\u003e\n\u003cli\u003ePadding、Container 内部结构\u003c/li\u003e\n\u003cli\u003eContainer 嵌套过多有什么性能问题\u003c/li\u003e\n\u003cli\u003eSizedBox、ConstrainedBox、UnconstrainedBox 用途\u003c/li\u003e\n\u003cli\u003eAspectRatio、FittedBox 缩放逻辑\u003c/li\u003e\n\u003cli\u003eWrap 自动换行实现原理\u003c/li\u003e\n\u003cli\u003eListView 几种构造方式区别（默认、builder、separated）\u003c/li\u003e\n\u003cli\u003eListView.builder 高性能原理，复用机制\u003c/li\u003e\n\u003cli\u003eListView 滚动复用失效场景\u003c/li\u003e\n\u003cli\u003eGridView 使用与性能优化\u003c/li\u003e\n\u003cli\u003eSingleChildScrollView 嵌套 ListView 报错解决方案\u003c/li\u003e\n\u003cli\u003eCustomScrollView、Sliver 体系是什么\u003c/li\u003e\n\u003cli\u003eSliverList、SliverGrid、SliverAppBar、SliverToBoxAdapter 使用场景\u003c/li\u003e\n\u003cli\u003eSliverPersistentHeader 固定吸顶实现\u003c/li\u003e\n\u003cli\u003eCustomScrollView 滚动统一原理\u003c/li\u003e\n\u003cli\u003eTable、DataTable 布局特性\u003c/li\u003e\n\u003cli\u003eIntrinsicHeight、IntrinsicWidth 性能损耗原因\u003c/li\u003e\n\u003cli\u003eLayoutBuilder 获取父布局约束场景\u003c/li\u003e\n\u003cli\u003eMediaQuery 获取设备信息，MediaQuery.of(context) 刷新机制\u003c/li\u003e\n\u003cli\u003eLayoutId、MultiChildLayout 自定义布局\u003c/li\u003e\n\u003cli\u003eFlow 高性能流式布局适用场景\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"四状态管理\"\u003e四、状态管理\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eState 自身状态管理优缺点\u003c/li\u003e\n\u003cli\u003eInheritedWidget 底层原理，数据传递逻辑\u003c/li\u003e\n\u003cli\u003eInheritedModel 对比 InheritedWidget 优势\u003c/li\u003e\n\u003cli\u003eProvider 整套库实现原理（ChangeNotifierProvider、Consumer、Selector）\u003c/li\u003e\n\u003cli\u003eSelector 为什么能精准局部刷新\u003c/li\u003e\n\u003cli\u003eChangeNotifier 通知更新机制\u003c/li\u003e\n\u003cli\u003eStateProvider、FutureProvider、StreamProvider 区别\u003c/li\u003e\n\u003cli\u003eRiverpod 对比 Provider 改进点\u003c/li\u003e\n\u003cli\u003eRiverpod 全局状态、自动 dispose、无 context 优势\u003c/li\u003e\n\u003cli\u003eStateNotifier、AsyncNotifier 使用场景\u003c/li\u003e\n\u003cli\u003eBloc 核心概念：Event、State、Bloc/Cubit\u003c/li\u003e\n\u003cli\u003eCubit 与 Bloc 区别\u003c/li\u003e\n\u003cli\u003eBlocProvider、BlocConsumer、BlocBuilder、BlocListener 分工\u003c/li\u003e\n\u003cli\u003eBloc 状态单一数据源优势\u003c/li\u003e\n\u003cli\u003eGetX 状态管理原理，GetController 生命周期\u003c/li\u003e\n\u003cli\u003eGetX 全局路由、依赖注入实现\u003c/li\u003e\n\u003cli\u003eGetX 优缺点，存在什么性能隐患\u003c/li\u003e\n\u003cli\u003eRedux 在 Flutter 中使用逻辑\u003c/li\u003e\n\u003cli\u003eMobX 响应式原理，Observable、Action、Reaction\u003c/li\u003e\n\u003cli\u003e大型项目如何选型状态管理方案\u003c/li\u003e\n\u003cli\u003e跨页面共享状态几种实现方式对比\u003c/li\u003e\n\u003cli\u003e状态持久化配合状态管理怎么做\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"五路由--导航\"\u003e五、路由 \u0026amp; 导航\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eNavigator 1.0 路由栈管理原理\u003c/li\u003e\n\u003cli\u003eMaterialPageRoute、CupertinoPageRoute 区别\u003c/li\u003e\n\u003cli\u003ePageRouteBuilder 自定义转场动画\u003c/li\u003e\n\u003cli\u003eNavigator push/pop/pushReplacement/popUntil 逻辑\u003c/li\u003e\n\u003cli\u003e路由传参几种方式\u003c/li\u003e\n\u003cli\u003eNavigator 2.0 核心组件：Router、RouteInformationParser、RouterDelegate\u003c/li\u003e\n\u003cli\u003eNavigator 2.0 完整路由流程\u003c/li\u003e\n\u003cli\u003eGoRouter 框架底层封装逻辑\u003c/li\u003e\n\u003cli\u003eGoRouter 嵌套路由、路由守卫、参数获取\u003c/li\u003e\n\u003cli\u003e路由栈持久化、页面状态保存方案\u003c/li\u003e\n\u003cli\u003e底部导航多 Tab 页面状态保持实现\u003c/li\u003e\n\u003cli\u003e路由拦截、登录鉴权统一处理\u003c/li\u003e\n\u003cli\u003e原生页面与 Flutter 页面互相跳转路由处理\u003c/li\u003e\n\u003cli\u003e页面返回监听 WillPopScope、PopScope\u003c/li\u003e\n\u003cli\u003e深度链接 deep link 适配方案\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"六动画\"\u003e六、动画\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eFlutter 动画核心类：Animation、AnimationController、CurvedAnimation\u003c/li\u003e\n\u003cli\u003eAnimationController vsync 作用，为什么需要 TickerProvider\u003c/li\u003e\n\u003cli\u003eSingleTickerProviderStateMixin、TickerProviderStateMixin 区别\u003c/li\u003e\n\u003cli\u003eTween、Animatable 差值计算原理\u003c/li\u003e\n\u003cli\u003e显式动画与隐式动画区别\u003c/li\u003e\n\u003cli\u003eAnimatedContainer、AnimatedOpacity 等隐式动画底层\u003c/li\u003e\n\u003cli\u003eHero 共享元素动画实现原理\u003c/li\u003e\n\u003cli\u003ePageRoute 共享 Hero 流程\u003c/li\u003e\n\u003cli\u003eCustomAnimation 自定义动画组件\u003c/li\u003e\n\u003cli\u003e交错动画 Interval 使用\u003c/li\u003e\n\u003cli\u003e动画性能优化，避免重建\u003c/li\u003e\n\u003cli\u003eLottie 动画集成原理，性能注意点\u003c/li\u003e\n\u003cli\u003e循环动画、反向动画实现\u003c/li\u003e\n\u003cli\u003e动画暂停、恢复、释放资源处理\u003c/li\u003e\n\u003cli\u003e自定义路由转场动画\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"七网络--本地存储\"\u003e七、网络 \u0026amp; 本地存储\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003edio 库封装思路，拦截器使用\u003c/li\u003e\n\u003cli\u003edio 统一请求头、错误处理、token 自动刷新\u003c/li\u003e\n\u003cli\u003e取消请求 CancelToken 使用场景\u003c/li\u003e\n\u003cli\u003e并发请求、超时处理\u003c/li\u003e\n\u003cli\u003eJson 序列化反序列化几种方式（手动、json_serializable）\u003c/li\u003e\n\u003cli\u003efromJson/toJson 生成流程\u003c/li\u003e\n\u003cli\u003eFreezed 数据模型优势\u003c/li\u003e\n\u003cli\u003ehttp 库与 dio 对比\u003c/li\u003e\n\u003cli\u003eWebSocket 在 Flutter 中使用，重连机制\u003c/li\u003e\n\u003cli\u003eshared_preferences 底层存储原理\u003c/li\u003e\n\u003cli\u003eHive 本地数据库优势，Box 使用\u003c/li\u003e\n\u003cli\u003eHive 加密、大数据存储优化\u003c/li\u003e\n\u003cli\u003esqflite 数据库操作，事务使用\u003c/li\u003e\n\u003cli\u003eIsolate 中操作数据库注意事项\u003c/li\u003e\n\u003cli\u003e文件读写 path_provider 获取路径\u003c/li\u003e\n\u003cli\u003e图片缓存 cached_network_image 实现逻辑\u003c/li\u003e\n\u003cli\u003e网络请求缓存策略设计\u003c/li\u003e\n\u003cli\u003e接口统一异常捕获封装方案\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"八原生交互-methodchannel\"\u003e八、原生交互 MethodChannel\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eMethodChannel、BasicMessageChannel、EventChannel 三者区别\u003c/li\u003e\n\u003cli\u003eMethodChannel 双向通信完整流程\u003c/li\u003e\n\u003cli\u003eFlutter 调用 Android 原生代码步骤\u003c/li\u003e\n\u003cli\u003eFlutter 调用 iOS OC/Swift 步骤\u003c/li\u003e\n\u003cli\u003e原生主动推送数据给 Flutter 使用哪个 Channel\u003c/li\u003e\n\u003cli\u003e传参类型限制，复杂对象传递方案\u003c/li\u003e\n\u003cli\u003e多页面 Channel 单例管理\u003c/li\u003e\n\u003cli\u003ePlatformView 嵌入原生 View（Android View、iOS UIView）\u003c/li\u003e\n\u003cli\u003ePlatformView 性能问题与优化\u003c/li\u003e\n\u003cli\u003e原生页面跳转 Flutter 传参\u003c/li\u003e\n\u003cli\u003e权限通过原生通道申请方案\u003c/li\u003e\n\u003cli\u003e混合栈开发：flutterBoost 原理\u003c/li\u003e\n\u003cli\u003eflutterBoost 页面生命周期同步问题\u003c/li\u003e\n\u003cli\u003e原生资源与 Flutter 资源冲突处理\u003c/li\u003e\n\u003cli\u003e通信通道内存泄漏如何避免\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"九性能优化专项\"\u003e九、性能优化专项\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eWidget 重建常见诱因与规避手段\u003c/li\u003e\n\u003cli\u003econst 构造优化原理\u003c/li\u003e\n\u003cli\u003eKey 不合理使用导致的性能问题\u003c/li\u003e\n\u003cli\u003eListView 长列表优化方案\u003c/li\u003e\n\u003cli\u003e图片加载优化：尺寸、缓存、压缩\u003c/li\u003e\n\u003cli\u003eRepaintBoundary 拆分重绘区域场景\u003c/li\u003e\n\u003cli\u003e减少 Layer 树层级优化\u003c/li\u003e\n\u003cli\u003e避免使用 Intrinsic 系列控件原因\u003c/li\u003e\n\u003cli\u003e动画卡顿排查思路\u003c/li\u003e\n\u003cli\u003eDevTools 工具使用：性能面板、内存、Widget 检查\u003c/li\u003e\n\u003cli\u003e内存泄漏常见场景（Timer、Stream、Channel、Animation）\u003c/li\u003e\n\u003cli\u003e大图片、大量对象内存释放方案\u003c/li\u003e\n\u003cli\u003eIsolate 处理耗时计算优化 UI\u003c/li\u003e\n\u003cli\u003ebuild 方法避免耗时操作\u003c/li\u003e\n\u003cli\u003e布局嵌套过深优化手段\u003c/li\u003e\n\u003cli\u003e页面 dispose 资源释放规范\u003c/li\u003e\n\u003cli\u003eProfile 模式性能排查要点\u003c/li\u003e\n\u003cli\u003e启动速度优化（包体积、懒加载、预加载）\u003c/li\u003e\n\u003cli\u003eFlutter 包体积优化方案\u003c/li\u003e\n\u003cli\u003e滑动卡顿完整排查流程\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十图片手势输入弹窗组件\"\u003e十、图片、手势、输入、弹窗组件\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eImage 几种构造：AssetImage、NetworkImage、FileImage、MemoryImage\u003c/li\u003e\n\u003cli\u003eImageProvider 缓存机制\u003c/li\u003e\n\u003cli\u003eImage.network 缓存缺陷，cached_network_image 弥补方案\u003c/li\u003e\n\u003cli\u003eDecorationImage、FadeInImage 使用\u003c/li\u003e\n\u003cli\u003eGestureDetector 手势识别原理，多手势冲突\u003c/li\u003e\n\u003cli\u003eListener 底层原始手势与 GestureDetector 区别\u003c/li\u003e\n\u003cli\u003e手势竞争、手势忽略场景\u003c/li\u003e\n\u003cli\u003eInkWell 水波纹实现原理，必须嵌套 Material\u003c/li\u003e\n\u003cli\u003eTextField 控制器 TextEditingController 使用\u003c/li\u003e\n\u003cli\u003eTextField 输入监听、格式化、光标控制\u003c/li\u003e\n\u003cli\u003e表单 Form、GlobalKey\u003cFormState\u003e 校验逻辑\u003c/li\u003e\n\u003cli\u003eModalRoute、showDialog、showBottomSheet 弹窗原理\u003c/li\u003e\n\u003cli\u003e弹窗状态共享问题解决方案\u003c/li\u003e\n\u003cli\u003eDraggable、LongPressDraggable 拖拽实现\u003c/li\u003e\n\u003cli\u003eTransform 变换不改变布局约束特性\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十一项目工程--打包适配\"\u003e十一、项目工程 \u0026amp; 打包适配\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003epubspec.yaml 各字段含义，依赖版本约束\u003c/li\u003e\n\u003cli\u003e本地依赖、Git 依赖、远程依赖配置\u003c/li\u003e\n\u003cli\u003epub 依赖冲突解决方法\u003c/li\u003e\n\u003cli\u003eFlutter 多环境配置（开发/测试/生产）\u003c/li\u003e\n\u003cli\u003eFlavor 多渠道打包 Android \u0026amp; iOS\u003c/li\u003e\n\u003cli\u003eAndroid 打包 keystore 配置，iOS 证书配置\u003c/li\u003e\n\u003cli\u003eAndroidManifest、Info.plist 权限适配\u003c/li\u003e\n\u003cli\u003e屏幕适配方案：媒体查询、比例、flutter_screenutil\u003c/li\u003e\n\u003cli\u003e深色模式适配实现\u003c/li\u003e\n\u003cli\u003e多语言国际化 intl 库使用\u003c/li\u003e\n\u003cli\u003eApp 图标、启动页配置\u003c/li\u003e\n\u003cli\u003eFlutter 插件开发流程\u003c/li\u003e\n\u003cli\u003e平台区分代码编写（dart.io、Platform 判断）\u003c/li\u003e\n\u003cli\u003e混合开发项目目录结构\u003c/li\u003e\n\u003cli\u003eCI/CD 自动打包脚本思路\u003c/li\u003e\n\u003cli\u003e版本号管理升级方案\u003c/li\u003e\n\u003cli\u003e第三方插件兼容问题处理\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十二进阶底层--疑难场景\"\u003e十二、进阶底层 \u0026amp; 疑难场景\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eWidgetsApp、MaterialApp、CupertinoApp 内部结构\u003c/li\u003e\n\u003cli\u003eThemeData 主题分发原理，局部主题覆盖\u003c/li\u003e\n\u003cli\u003eNavigator 上下文与页面上下文区别\u003c/li\u003e\n\u003cli\u003econtext 获取上层 Widget 原理，BuildContext 分类\u003c/li\u003e\n\u003cli\u003eState 保存在 Element 中，Widget 重建 State 不销毁条件\u003c/li\u003e\n\u003cli\u003eOffstage、Visibility 控件隐藏区别\u003c/li\u003e\n\u003cli\u003eAbsorbPointer、IgnorePointer 拦截手势差异\u003c/li\u003e\n\u003cli\u003eClip 裁剪控件：ClipRect、ClipRRect、ClipOval 性能\u003c/li\u003e\n\u003cli\u003eBackdropFilter 模糊控件性能损耗原因\u003c/li\u003e\n\u003cli\u003eRenderBox 自定义控件完整流程\u003c/li\u003e\n\u003cli\u003e自定义 RenderObject 实现步骤\u003c/li\u003e\n\u003cli\u003e自定义 MultiChildRenderObjectWidget\u003c/li\u003e\n\u003cli\u003eScrollController 监听滚动、滚动定位\u003c/li\u003e\n\u003cli\u003eScrollPhysics 滚动回弹、阻尼自定义\u003c/li\u003e\n\u003cli\u003e监听应用前后台切换 AppLifecycleState\u003c/li\u003e\n\u003cli\u003e截屏、屏幕亮度、震动原生能力实现\u003c/li\u003e\n\u003cli\u003eWebView 插件使用与通信\u003c/li\u003e\n\u003cli\u003eFlutter Web 编译差异，平台兼容问题\u003c/li\u003e\n\u003cli\u003eFlutter Windows/macOS/Linux 桌面端适配要点\u003c/li\u003e\n\u003cli\u003e如何实现全局捕获异常\u003c/li\u003e\n\u003cli\u003eTimer、StreamSubscription 忘记取消的内存泄漏\u003c/li\u003e\n\u003cli\u003e列表 item 点击状态保存方案\u003c/li\u003e\n\u003cli\u003e异步并发竞态条件问题处理\u003c/li\u003e\n\u003cli\u003e图片大量加载 OOM 解决方案\u003c/li\u003e\n\u003cli\u003e原生弹窗遮挡 Flutter 页面问题\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十三综合项目面试问答题\"\u003e十三、综合项目面试问答题\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e讲一下你项目整体架构分层\u003c/li\u003e\n\u003cli\u003e项目状态管理选型原因\u003c/li\u003e\n\u003cli\u003e长列表卡顿怎么优化的\u003c/li\u003e\n\u003cli\u003e多页面共享登录状态如何实现\u003c/li\u003e\n\u003cli\u003etoken 过期统一拦截处理方案\u003c/li\u003e\n\u003cli\u003e项目混合栈开发遇到哪些坑\u003c/li\u003e\n\u003cli\u003e图片缓存、网络缓存怎么设计\u003c/li\u003e\n\u003cli\u003e全局网络加载弹窗、错误页封装思路\u003c/li\u003e\n\u003cli\u003e项目通用业务组件封装思路\u003c/li\u003e\n\u003cli\u003e线上卡顿、崩溃排查处理经验\u003c/li\u003e\n\u003cli\u003e如何实现页面无网络重试逻辑\u003c/li\u003e\n\u003cli\u003e复杂表单页面状态管理方案\u003c/li\u003e\n\u003cli\u003e项目包体积优化做了哪些操作\u003c/li\u003e\n\u003cli\u003e多渠道、多环境配置实现\u003c/li\u003e\n\u003cli\u003e封装通用 Dio 请求库完整思路\u003c/li\u003e\n\u003c/ol\u003e","title":"Flutter 相关面试题整理"},{"content":"Mybatis 执行流程 mybatis-config.xml (总规) ↓ SqlSessionFactory (总管) ↓ SqlSession (服务员) —— 包含所有执行方法 ↓ Executor (大管家) —— 优先查缓存（一级/二级） ↓ MappedStatement (菜谱) —— 持有SQL、参数映射、结果映射 ↓ 输入参数映射 (备菜) → JDBC执行 → 输出结果映射 (装盘) ↓ 返回POJO/集合 (上菜)\nMybatis 是否支持延迟加载 支持，默认不开启\ncollection fecthType=\u0026ldquo;lazy\u0026rdquo;\nmybatis配置文件延迟加载 lazyLoadingEnabled\n延迟加载的原理\nCGLIB创建目标对象的代理对象 调用目标方法 getOrderList 调用对象 invoke 方法 判断 orderList 是否为空 为空，执行SQL 不为空获取结果 封装 orderList Mybatis 的一级缓存 二级缓存 用过吗 本地缓存 - 基于 PerpetualCache 本质是一个 HashMap 一级缓存 - 作用域是 session 级别 二级缓存 - 作用域是namespace 和 mapper的作用域，不依赖于 session 二级缓存默认关闭 settings name=\u0026ldquo;cacheEnabled\u0026rdquo; value=\u0026ldquo;true\u0026rdquo; 当某个作用域进行了增删改操作，该作用域下的所有select的缓存将被clear\n","permalink":"https://lv-blog.pages.dev/posts/programming/for-interview/mybatis-interview-questions/","summary":"\u003ch2 id=\"mybatis-执行流程\"\u003eMybatis 执行流程\u003c/h2\u003e\n\u003cp\u003emybatis-config.xml (总规)\n↓\nSqlSessionFactory (总管)\n↓\nSqlSession (服务员) —— 包含所有执行方法\n↓\nExecutor (大管家) —— 优先查缓存（一级/二级）\n↓\nMappedStatement (菜谱) —— 持有SQL、参数映射、结果映射\n↓\n输入参数映射 (备菜) → JDBC执行 → 输出结果映射 (装盘)\n↓\n返回POJO/集合 (上菜)\u003c/p\u003e\n\u003ch2 id=\"mybatis-是否支持延迟加载\"\u003eMybatis 是否支持延迟加载\u003c/h2\u003e\n\u003cp\u003e支持，默认不开启\u003c/p\u003e\n\u003cp\u003ecollection fecthType=\u0026ldquo;lazy\u0026rdquo;\u003c/p\u003e\n\u003cp\u003emybatis配置文件延迟加载 lazyLoadingEnabled\u003c/p\u003e\n\u003cp\u003e延迟加载的原理\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003eCGLIB创建目标对象的代理对象\u003c/li\u003e\n\u003cli\u003e调用目标方法 getOrderList\u003c/li\u003e\n\u003cli\u003e调用对象 invoke 方法\u003c/li\u003e\n\u003cli\u003e判断 orderList 是否为空\u003c/li\u003e\n\u003cli\u003e为空，执行SQL\u003c/li\u003e\n\u003cli\u003e不为空获取结果\u003c/li\u003e\n\u003cli\u003e封装 orderList\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"mybatis-的一级缓存-二级缓存-用过吗\"\u003eMybatis 的一级缓存 二级缓存 用过吗\u003c/h2\u003e\n\u003cp\u003e本地缓存 - 基于 PerpetualCache 本质是一个 HashMap\n一级缓存 - 作用域是 session 级别\n二级缓存 - 作用域是namespace 和 mapper的作用域，不依赖于 session\n二级缓存默认关闭\nsettings name=\u0026ldquo;cacheEnabled\u0026rdquo; value=\u0026ldquo;true\u0026rdquo;\n当某个作用域进行了增删改操作，该作用域下的所有select的缓存将被clear\u003c/p\u003e","title":"MyBatis 相关面试题整理"},{"content":" 从互联网整理而来\n一、JS 基础前置 原型与原型链 闭包、闭包引发的内存泄漏场景 同步异步、宏任务微任务执行顺序 Promise 状态、静态方法、链式调用 async/await 原理 深浅拷贝实现与区别 this 指向五种场景 箭头函数与普通函数差异 ES6 解构、展开运算符、剩余参数 var / let / const 区别 CommonJS 与 ES Module 差异 TypeScript 接口、泛型、交叉类型、联合类型 EventLoop 完整执行流程 防抖节流实现原理 纯函数定义与特性 二、React 基础核心概念 React 设计思想与核心理念 虚拟 DOM 是什么，作用与优势 JSX 语法本质，编译产物 JSX 中 {} 可放入哪些内容，不能放什么 React 元素与组件区别 函数组件与类组件区别 纯组件 PureComponent 原理 React 单向数据流含义 props 只读特性，为什么不能直接修改 props.children 有哪些类型 key 的作用，为什么不能用 index 做 key React 列表渲染注意事项 className 代替 class、htmlFor 代替 for 原因 React 事件绑定与原生 DOM 事件差异 合成事件原理、事件委托机制 阻止合成事件冒泡几种方式 dangerouslySetInnerHTML 使用场景与风险 React 为什么不直接操作 DOM Fragment 作用，短语法 \u0026lt;\u0026gt; React 严格模式 StrictMode 作用 三、类组件生命周期（Class Component） 类组件完整生命周期分哪三个阶段 constructor 执行时机与作用 static getDerivedStateFromProps 执行时机与用途 shouldComponentUpdate 返回值作用 render 执行规则，禁止在其中修改 state componentDidMount 执行时机、常用场景 getSnapshotBeforeUpdate 触发时机、返回值用途 componentDidUpdate 参数含义，依赖判断写法 componentWillUnmount 清理资源场景 废弃生命周期函数有哪些，为什么废弃 props 变化触发的生命周期流程 setState 同步还是异步，底层批量更新逻辑 setState 两种传参形式（对象/函数）区别 多次连续 setState 合并规则 如何在 setState 后拿到最新 state 四、Hooks 全套（React16.8+） Hook 诞生背景，解决类组件什么痛点 Hook 使用两条硬性规则 useState 底层原理，为何数组解构不受顺序影响 useState 传入函数初始化的优势 useEffect 作用，依赖数组含义 useEffect 模拟生命周期分别怎么写 useEffect 返回清理函数执行时机 依赖数组漏传变量引发的闭包陷阱 useLayoutEffect 和 useEffect 执行顺序、区别 useMemo 缓存计算值，解决什么问题 useCallback 缓存函数，搭配子组件使用场景 useMemo 与 useCallback 性能误区 useRef 存储DOM、存储可变数据两种用法 ref 传递 forwardRef 使用场景 useImperativeHandle 作用 useContext 跨层级传值，使用缺陷 useReducer 适用场景，和 useState 对比 useReducer 简单实现思路 useId 作用，解决什么问题 useTransition 区分紧急/非紧急更新 useDeferredValue 延迟计算值 useSyncExternalStore 订阅外部数据源 useDebugValue 自定义Hook调试展示 自定义 Hook 封装规范，复用逻辑思路 Hook 闭包陷阱完整成因与解决方案 五、React18 新特性 React18 并发渲染 Concurrent Mode 含义 自动批处理更新范围变化 createRoot 替换 legacy root 差异 Suspense 服务端/客户端用法 Transitions API 解决场景 自动插入批处理，异步回调内也合并更新 useTransition 与 useDeferredValue 区别 服务端组件 RSC 基础概念 StrictMode 双重渲染机制目的 批量更新边界，何时不会合并state更新 六、Diff 算法与渲染更新 React Diff 三大策略 同层比较，不跨层级对比原因 无key、index key、唯一key三种场景对比 节点删除、新增、移动判断逻辑 文本节点、组件节点 Diff 区别 列表 Diff 优化逻辑 什么情况下组件会重新渲染 如何避免不必要的重渲染 memo、useMemo、useCallback 配合使用完整逻辑 渲染流程：render → 生成VNode → Diff → 真实DOM更新 七、组件通信方案 父子组件 props 传值、回调传方法 多层级透传 props 缺点 Context API 跨层级通信优缺点 Context 配合 useReducer 简易状态管理 父访问子组件实例 ref 子调用父方法回调函数 兄弟组件通信几种方案 全局事件总线实现与缺陷 跨页面全局状态管理方案对比 八、状态管理 Redux / Redux Toolkit / Zustand Redux 三大核心原则 Redux 五大核心概念：Store、Action、Reducer、Dispatch、State Reducer 必须纯函数要求 Action plain object 规范，为什么不能异步 中间件 middleware 作用，执行流程 redux-thunk 异步处理原理 redux-saga 对比 thunk 优势 combineReducers 拆分模块原理 react-redux 核心 API Provider、connect mapStateToProps、mapDispatchToProps 作用 useSelector、useDispatch Hook 用法 Redux 数据单向流动完整流程 Redux Toolkit 解决原生Redux哪些痛点 createSlice 内置 reducer、action 生成 RTK 内置immer 实现可变写法原理 RTK Query 接口缓存、请求管理 Zustand 轻量状态库对比Redux优势 状态持久化 redux-persist 流程 大型项目状态库选型思路 多组件共享全局状态最佳实践 九、React Router v6 v5 与 v6 核心改动差异 createBrowserRouter 路由配置方式 BrowserRouter、HashRouter、MemoryRouter 区别 Routes 替代 Switch 作用 Route element 传组件写法 useNavigate 替代 useHistory 路由传参 params、search、state 三种方式 useParams、useSearchParams 使用 嵌套路由 Outlet 占位原理 index 索引路由作用 Link、NavLink 区别 路由守卫鉴权封装方案 路由懒加载 + Suspense 配置 useLocation 获取路由信息 动态路由、路由拦截封装 十、表单处理 受控组件与非受控组件定义、区别 受控组件完整实现思路 非受控组件通过 ref 获取值场景 多表单项统一封装受控逻辑 第三方表单库 Formik / React Hook Form 对比 React Hook Form 非受控高性能原理 表单校验方案：原生、yup、zod 表单批量重置、回填数据处理 十一、样式方案 inline 内联样式优缺点 CSS Modules 样式隔离原理 styled-components CSS-in-JS 原理 emotion 与 styled-components 对比 tailwindcss 在React项目使用优势 动态样式几种实现方式 全局样式污染解决方案 十二、工程化构建 Create React App 底层 webpack 配置 Vite 构建React项目优势，esbuild预构建 vite.config.js 常用配置项 webpack 处理jsx、ts-loader流程 babel 转换 JSX、polyfill 原理 .env 环境变量区分开发/测试/生产 跨域代理 proxy 配置 路由懒加载分包代码分割 Tree-Shaking 生效条件 打包体积优化手段 第三方组件库按需引入配置 unplugin-auto-import 自动导入hooks 十三、网络请求封装 axios 请求拦截、响应拦截封装 全局 loading、统一错误处理 token 过期无感刷新实现逻辑 重复请求取消方案 CancelToken 请求并发处理 封装自定义请求Hook SWR / React Query 数据请求缓存库原理 React Query 自动重请求、缓存失效策略 十四、性能优化专题 组件无效重渲染全部诱因 memo 缓存函数组件条件 useCallback 缓存函数使用场景 useMemo 缓存计算结果适用场景 列表渲染优化策略 虚拟列表 react-window / react-virtualized 原理 图片懒加载实现 路由懒加载分包优化首屏 大组件拆分、状态下沉优化 避免在render内创建函数/对象 React DevTools Profiler 排查卡顿 减少Context频繁更新范围 定时器、订阅事件组件卸载清理 打包体积压缩：gzip、剔除无用依赖 并发更新优化用户交互体验 十五、进阶底层原理 Fiber 架构解决旧架构什么问题 Fiber 节点结构、任务拆分机制 协调阶段、提交阶段分工 可中断、可恢复、可优先级调度原理 调和过程完整流程 批量更新实现机制 合成事件系统底层池化机制 React 事务机制 Portal 传送门实现原理，事件冒泡特点 ErrorBoundary 错误捕获范围，无法捕获哪些错误 React 服务端渲染 SSR 流程 Next.js 与 CRA 区别，SSR/SSG/ISR 概念 Hydrate 水合作用，水合不匹配成因 RSC React Server Components 运行机制 微前端中 React 应用隔离方案 十六、项目实战场景题 项目目录分层架构设计 状态管理选型理由 大型表单封装思路 页面按钮、接口权限控制实现 首屏加载缓慢优化方案 列表大量数据渲染卡顿解决 全局弹窗、消息提示封装 路由鉴权、未登录跳转逻辑 组件复用抽离自定义Hook规范 线上报错捕获监控方案 多环境变量、多渠道打包配置 防抖节流在搜索框场景实现 页面返回保留表单状态实现 重复提交接口拦截处理 React 项目迁移 TS 改造思路 ","permalink":"https://lv-blog.pages.dev/posts/programming/frontend/react-interview-questions/","summary":"\u003cblockquote\u003e\n\u003cp\u003e从互联网整理而来\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"一js-基础前置\"\u003e一、JS 基础前置\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e原型与原型链\u003c/li\u003e\n\u003cli\u003e闭包、闭包引发的内存泄漏场景\u003c/li\u003e\n\u003cli\u003e同步异步、宏任务微任务执行顺序\u003c/li\u003e\n\u003cli\u003ePromise 状态、静态方法、链式调用\u003c/li\u003e\n\u003cli\u003easync/await 原理\u003c/li\u003e\n\u003cli\u003e深浅拷贝实现与区别\u003c/li\u003e\n\u003cli\u003ethis 指向五种场景\u003c/li\u003e\n\u003cli\u003e箭头函数与普通函数差异\u003c/li\u003e\n\u003cli\u003eES6 解构、展开运算符、剩余参数\u003c/li\u003e\n\u003cli\u003evar / let / const 区别\u003c/li\u003e\n\u003cli\u003eCommonJS 与 ES Module 差异\u003c/li\u003e\n\u003cli\u003eTypeScript 接口、泛型、交叉类型、联合类型\u003c/li\u003e\n\u003cli\u003eEventLoop 完整执行流程\u003c/li\u003e\n\u003cli\u003e防抖节流实现原理\u003c/li\u003e\n\u003cli\u003e纯函数定义与特性\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"二react-基础核心概念\"\u003e二、React 基础核心概念\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eReact 设计思想与核心理念\u003c/li\u003e\n\u003cli\u003e虚拟 DOM 是什么，作用与优势\u003c/li\u003e\n\u003cli\u003eJSX 语法本质，编译产物\u003c/li\u003e\n\u003cli\u003eJSX 中 {} 可放入哪些内容，不能放什么\u003c/li\u003e\n\u003cli\u003eReact 元素与组件区别\u003c/li\u003e\n\u003cli\u003e函数组件与类组件区别\u003c/li\u003e\n\u003cli\u003e纯组件 PureComponent 原理\u003c/li\u003e\n\u003cli\u003eReact 单向数据流含义\u003c/li\u003e\n\u003cli\u003eprops 只读特性，为什么不能直接修改\u003c/li\u003e\n\u003cli\u003eprops.children 有哪些类型\u003c/li\u003e\n\u003cli\u003ekey 的作用，为什么不能用 index 做 key\u003c/li\u003e\n\u003cli\u003eReact 列表渲染注意事项\u003c/li\u003e\n\u003cli\u003eclassName 代替 class、htmlFor 代替 for 原因\u003c/li\u003e\n\u003cli\u003eReact 事件绑定与原生 DOM 事件差异\u003c/li\u003e\n\u003cli\u003e合成事件原理、事件委托机制\u003c/li\u003e\n\u003cli\u003e阻止合成事件冒泡几种方式\u003c/li\u003e\n\u003cli\u003edangerouslySetInnerHTML 使用场景与风险\u003c/li\u003e\n\u003cli\u003eReact 为什么不直接操作 DOM\u003c/li\u003e\n\u003cli\u003eFragment 作用，短语法 \u0026lt;\u0026gt;\u003c/li\u003e\n\u003cli\u003eReact 严格模式 StrictMode 作用\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"三类组件生命周期class-component\"\u003e三、类组件生命周期（Class Component）\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e类组件完整生命周期分哪三个阶段\u003c/li\u003e\n\u003cli\u003econstructor 执行时机与作用\u003c/li\u003e\n\u003cli\u003estatic getDerivedStateFromProps 执行时机与用途\u003c/li\u003e\n\u003cli\u003eshouldComponentUpdate 返回值作用\u003c/li\u003e\n\u003cli\u003erender 执行规则，禁止在其中修改 state\u003c/li\u003e\n\u003cli\u003ecomponentDidMount 执行时机、常用场景\u003c/li\u003e\n\u003cli\u003egetSnapshotBeforeUpdate 触发时机、返回值用途\u003c/li\u003e\n\u003cli\u003ecomponentDidUpdate 参数含义，依赖判断写法\u003c/li\u003e\n\u003cli\u003ecomponentWillUnmount 清理资源场景\u003c/li\u003e\n\u003cli\u003e废弃生命周期函数有哪些，为什么废弃\u003c/li\u003e\n\u003cli\u003eprops 变化触发的生命周期流程\u003c/li\u003e\n\u003cli\u003esetState 同步还是异步，底层批量更新逻辑\u003c/li\u003e\n\u003cli\u003esetState 两种传参形式（对象/函数）区别\u003c/li\u003e\n\u003cli\u003e多次连续 setState 合并规则\u003c/li\u003e\n\u003cli\u003e如何在 setState 后拿到最新 state\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"四hooks-全套react168\"\u003e四、Hooks 全套（React16.8+）\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eHook 诞生背景，解决类组件什么痛点\u003c/li\u003e\n\u003cli\u003eHook 使用两条硬性规则\u003c/li\u003e\n\u003cli\u003euseState 底层原理，为何数组解构不受顺序影响\u003c/li\u003e\n\u003cli\u003euseState 传入函数初始化的优势\u003c/li\u003e\n\u003cli\u003euseEffect 作用，依赖数组含义\u003c/li\u003e\n\u003cli\u003euseEffect 模拟生命周期分别怎么写\u003c/li\u003e\n\u003cli\u003euseEffect 返回清理函数执行时机\u003c/li\u003e\n\u003cli\u003e依赖数组漏传变量引发的闭包陷阱\u003c/li\u003e\n\u003cli\u003euseLayoutEffect 和 useEffect 执行顺序、区别\u003c/li\u003e\n\u003cli\u003euseMemo 缓存计算值，解决什么问题\u003c/li\u003e\n\u003cli\u003euseCallback 缓存函数，搭配子组件使用场景\u003c/li\u003e\n\u003cli\u003euseMemo 与 useCallback 性能误区\u003c/li\u003e\n\u003cli\u003euseRef 存储DOM、存储可变数据两种用法\u003c/li\u003e\n\u003cli\u003eref 传递 forwardRef 使用场景\u003c/li\u003e\n\u003cli\u003euseImperativeHandle 作用\u003c/li\u003e\n\u003cli\u003euseContext 跨层级传值，使用缺陷\u003c/li\u003e\n\u003cli\u003euseReducer 适用场景，和 useState 对比\u003c/li\u003e\n\u003cli\u003euseReducer 简单实现思路\u003c/li\u003e\n\u003cli\u003euseId 作用，解决什么问题\u003c/li\u003e\n\u003cli\u003euseTransition 区分紧急/非紧急更新\u003c/li\u003e\n\u003cli\u003euseDeferredValue 延迟计算值\u003c/li\u003e\n\u003cli\u003euseSyncExternalStore 订阅外部数据源\u003c/li\u003e\n\u003cli\u003euseDebugValue 自定义Hook调试展示\u003c/li\u003e\n\u003cli\u003e自定义 Hook 封装规范，复用逻辑思路\u003c/li\u003e\n\u003cli\u003eHook 闭包陷阱完整成因与解决方案\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"五react18-新特性\"\u003e五、React18 新特性\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eReact18 并发渲染 Concurrent Mode 含义\u003c/li\u003e\n\u003cli\u003e自动批处理更新范围变化\u003c/li\u003e\n\u003cli\u003ecreateRoot 替换 legacy root 差异\u003c/li\u003e\n\u003cli\u003eSuspense 服务端/客户端用法\u003c/li\u003e\n\u003cli\u003eTransitions API 解决场景\u003c/li\u003e\n\u003cli\u003e自动插入批处理，异步回调内也合并更新\u003c/li\u003e\n\u003cli\u003euseTransition 与 useDeferredValue 区别\u003c/li\u003e\n\u003cli\u003e服务端组件 RSC 基础概念\u003c/li\u003e\n\u003cli\u003eStrictMode 双重渲染机制目的\u003c/li\u003e\n\u003cli\u003e批量更新边界，何时不会合并state更新\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"六diff-算法与渲染更新\"\u003e六、Diff 算法与渲染更新\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eReact Diff 三大策略\u003c/li\u003e\n\u003cli\u003e同层比较，不跨层级对比原因\u003c/li\u003e\n\u003cli\u003e无key、index key、唯一key三种场景对比\u003c/li\u003e\n\u003cli\u003e节点删除、新增、移动判断逻辑\u003c/li\u003e\n\u003cli\u003e文本节点、组件节点 Diff 区别\u003c/li\u003e\n\u003cli\u003e列表 Diff 优化逻辑\u003c/li\u003e\n\u003cli\u003e什么情况下组件会重新渲染\u003c/li\u003e\n\u003cli\u003e如何避免不必要的重渲染\u003c/li\u003e\n\u003cli\u003ememo、useMemo、useCallback 配合使用完整逻辑\u003c/li\u003e\n\u003cli\u003e渲染流程：render → 生成VNode → Diff → 真实DOM更新\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"七组件通信方案\"\u003e七、组件通信方案\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e父子组件 props 传值、回调传方法\u003c/li\u003e\n\u003cli\u003e多层级透传 props 缺点\u003c/li\u003e\n\u003cli\u003eContext API 跨层级通信优缺点\u003c/li\u003e\n\u003cli\u003eContext 配合 useReducer 简易状态管理\u003c/li\u003e\n\u003cli\u003e父访问子组件实例 ref\u003c/li\u003e\n\u003cli\u003e子调用父方法回调函数\u003c/li\u003e\n\u003cli\u003e兄弟组件通信几种方案\u003c/li\u003e\n\u003cli\u003e全局事件总线实现与缺陷\u003c/li\u003e\n\u003cli\u003e跨页面全局状态管理方案对比\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"八状态管理-redux--redux-toolkit--zustand\"\u003e八、状态管理 Redux / Redux Toolkit / Zustand\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eRedux 三大核心原则\u003c/li\u003e\n\u003cli\u003eRedux 五大核心概念：Store、Action、Reducer、Dispatch、State\u003c/li\u003e\n\u003cli\u003eReducer 必须纯函数要求\u003c/li\u003e\n\u003cli\u003eAction plain object 规范，为什么不能异步\u003c/li\u003e\n\u003cli\u003e中间件 middleware 作用，执行流程\u003c/li\u003e\n\u003cli\u003eredux-thunk 异步处理原理\u003c/li\u003e\n\u003cli\u003eredux-saga 对比 thunk 优势\u003c/li\u003e\n\u003cli\u003ecombineReducers 拆分模块原理\u003c/li\u003e\n\u003cli\u003ereact-redux 核心 API Provider、connect\u003c/li\u003e\n\u003cli\u003emapStateToProps、mapDispatchToProps 作用\u003c/li\u003e\n\u003cli\u003euseSelector、useDispatch Hook 用法\u003c/li\u003e\n\u003cli\u003eRedux 数据单向流动完整流程\u003c/li\u003e\n\u003cli\u003eRedux Toolkit 解决原生Redux哪些痛点\u003c/li\u003e\n\u003cli\u003ecreateSlice 内置 reducer、action 生成\u003c/li\u003e\n\u003cli\u003eRTK 内置immer 实现可变写法原理\u003c/li\u003e\n\u003cli\u003eRTK Query 接口缓存、请求管理\u003c/li\u003e\n\u003cli\u003eZustand 轻量状态库对比Redux优势\u003c/li\u003e\n\u003cli\u003e状态持久化 redux-persist 流程\u003c/li\u003e\n\u003cli\u003e大型项目状态库选型思路\u003c/li\u003e\n\u003cli\u003e多组件共享全局状态最佳实践\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"九react-router-v6\"\u003e九、React Router v6\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003ev5 与 v6 核心改动差异\u003c/li\u003e\n\u003cli\u003ecreateBrowserRouter 路由配置方式\u003c/li\u003e\n\u003cli\u003eBrowserRouter、HashRouter、MemoryRouter 区别\u003c/li\u003e\n\u003cli\u003eRoutes 替代 Switch 作用\u003c/li\u003e\n\u003cli\u003eRoute element 传组件写法\u003c/li\u003e\n\u003cli\u003euseNavigate 替代 useHistory\u003c/li\u003e\n\u003cli\u003e路由传参 params、search、state 三种方式\u003c/li\u003e\n\u003cli\u003euseParams、useSearchParams 使用\u003c/li\u003e\n\u003cli\u003e嵌套路由 Outlet 占位原理\u003c/li\u003e\n\u003cli\u003eindex 索引路由作用\u003c/li\u003e\n\u003cli\u003eLink、NavLink 区别\u003c/li\u003e\n\u003cli\u003e路由守卫鉴权封装方案\u003c/li\u003e\n\u003cli\u003e路由懒加载 + Suspense 配置\u003c/li\u003e\n\u003cli\u003euseLocation 获取路由信息\u003c/li\u003e\n\u003cli\u003e动态路由、路由拦截封装\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十表单处理\"\u003e十、表单处理\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e受控组件与非受控组件定义、区别\u003c/li\u003e\n\u003cli\u003e受控组件完整实现思路\u003c/li\u003e\n\u003cli\u003e非受控组件通过 ref 获取值场景\u003c/li\u003e\n\u003cli\u003e多表单项统一封装受控逻辑\u003c/li\u003e\n\u003cli\u003e第三方表单库 Formik / React Hook Form 对比\u003c/li\u003e\n\u003cli\u003eReact Hook Form 非受控高性能原理\u003c/li\u003e\n\u003cli\u003e表单校验方案：原生、yup、zod\u003c/li\u003e\n\u003cli\u003e表单批量重置、回填数据处理\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十一样式方案\"\u003e十一、样式方案\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003einline 内联样式优缺点\u003c/li\u003e\n\u003cli\u003eCSS Modules 样式隔离原理\u003c/li\u003e\n\u003cli\u003estyled-components CSS-in-JS 原理\u003c/li\u003e\n\u003cli\u003eemotion 与 styled-components 对比\u003c/li\u003e\n\u003cli\u003etailwindcss 在React项目使用优势\u003c/li\u003e\n\u003cli\u003e动态样式几种实现方式\u003c/li\u003e\n\u003cli\u003e全局样式污染解决方案\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十二工程化构建\"\u003e十二、工程化构建\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eCreate React App 底层 webpack 配置\u003c/li\u003e\n\u003cli\u003eVite 构建React项目优势，esbuild预构建\u003c/li\u003e\n\u003cli\u003evite.config.js 常用配置项\u003c/li\u003e\n\u003cli\u003ewebpack 处理jsx、ts-loader流程\u003c/li\u003e\n\u003cli\u003ebabel 转换 JSX、polyfill 原理\u003c/li\u003e\n\u003cli\u003e.env 环境变量区分开发/测试/生产\u003c/li\u003e\n\u003cli\u003e跨域代理 proxy 配置\u003c/li\u003e\n\u003cli\u003e路由懒加载分包代码分割\u003c/li\u003e\n\u003cli\u003eTree-Shaking 生效条件\u003c/li\u003e\n\u003cli\u003e打包体积优化手段\u003c/li\u003e\n\u003cli\u003e第三方组件库按需引入配置\u003c/li\u003e\n\u003cli\u003eunplugin-auto-import 自动导入hooks\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十三网络请求封装\"\u003e十三、网络请求封装\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eaxios 请求拦截、响应拦截封装\u003c/li\u003e\n\u003cli\u003e全局 loading、统一错误处理\u003c/li\u003e\n\u003cli\u003etoken 过期无感刷新实现逻辑\u003c/li\u003e\n\u003cli\u003e重复请求取消方案 CancelToken\u003c/li\u003e\n\u003cli\u003e请求并发处理\u003c/li\u003e\n\u003cli\u003e封装自定义请求Hook\u003c/li\u003e\n\u003cli\u003eSWR / React Query 数据请求缓存库原理\u003c/li\u003e\n\u003cli\u003eReact Query 自动重请求、缓存失效策略\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十四性能优化专题\"\u003e十四、性能优化专题\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e组件无效重渲染全部诱因\u003c/li\u003e\n\u003cli\u003ememo 缓存函数组件条件\u003c/li\u003e\n\u003cli\u003euseCallback 缓存函数使用场景\u003c/li\u003e\n\u003cli\u003euseMemo 缓存计算结果适用场景\u003c/li\u003e\n\u003cli\u003e列表渲染优化策略\u003c/li\u003e\n\u003cli\u003e虚拟列表 react-window / react-virtualized 原理\u003c/li\u003e\n\u003cli\u003e图片懒加载实现\u003c/li\u003e\n\u003cli\u003e路由懒加载分包优化首屏\u003c/li\u003e\n\u003cli\u003e大组件拆分、状态下沉优化\u003c/li\u003e\n\u003cli\u003e避免在render内创建函数/对象\u003c/li\u003e\n\u003cli\u003eReact DevTools Profiler 排查卡顿\u003c/li\u003e\n\u003cli\u003e减少Context频繁更新范围\u003c/li\u003e\n\u003cli\u003e定时器、订阅事件组件卸载清理\u003c/li\u003e\n\u003cli\u003e打包体积压缩：gzip、剔除无用依赖\u003c/li\u003e\n\u003cli\u003e并发更新优化用户交互体验\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十五进阶底层原理\"\u003e十五、进阶底层原理\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eFiber 架构解决旧架构什么问题\u003c/li\u003e\n\u003cli\u003eFiber 节点结构、任务拆分机制\u003c/li\u003e\n\u003cli\u003e协调阶段、提交阶段分工\u003c/li\u003e\n\u003cli\u003e可中断、可恢复、可优先级调度原理\u003c/li\u003e\n\u003cli\u003e调和过程完整流程\u003c/li\u003e\n\u003cli\u003e批量更新实现机制\u003c/li\u003e\n\u003cli\u003e合成事件系统底层池化机制\u003c/li\u003e\n\u003cli\u003eReact 事务机制\u003c/li\u003e\n\u003cli\u003ePortal 传送门实现原理，事件冒泡特点\u003c/li\u003e\n\u003cli\u003eErrorBoundary 错误捕获范围，无法捕获哪些错误\u003c/li\u003e\n\u003cli\u003eReact 服务端渲染 SSR 流程\u003c/li\u003e\n\u003cli\u003eNext.js 与 CRA 区别，SSR/SSG/ISR 概念\u003c/li\u003e\n\u003cli\u003eHydrate 水合作用，水合不匹配成因\u003c/li\u003e\n\u003cli\u003eRSC React Server Components 运行机制\u003c/li\u003e\n\u003cli\u003e微前端中 React 应用隔离方案\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十六项目实战场景题\"\u003e十六、项目实战场景题\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e项目目录分层架构设计\u003c/li\u003e\n\u003cli\u003e状态管理选型理由\u003c/li\u003e\n\u003cli\u003e大型表单封装思路\u003c/li\u003e\n\u003cli\u003e页面按钮、接口权限控制实现\u003c/li\u003e\n\u003cli\u003e首屏加载缓慢优化方案\u003c/li\u003e\n\u003cli\u003e列表大量数据渲染卡顿解决\u003c/li\u003e\n\u003cli\u003e全局弹窗、消息提示封装\u003c/li\u003e\n\u003cli\u003e路由鉴权、未登录跳转逻辑\u003c/li\u003e\n\u003cli\u003e组件复用抽离自定义Hook规范\u003c/li\u003e\n\u003cli\u003e线上报错捕获监控方案\u003c/li\u003e\n\u003cli\u003e多环境变量、多渠道打包配置\u003c/li\u003e\n\u003cli\u003e防抖节流在搜索框场景实现\u003c/li\u003e\n\u003cli\u003e页面返回保留表单状态实现\u003c/li\u003e\n\u003cli\u003e重复提交接口拦截处理\u003c/li\u003e\n\u003cli\u003eReact 项目迁移 TS 改造思路\u003c/li\u003e\n\u003c/ol\u003e","title":"React 相关面试题整理"},{"content":"你的项目的哪些场景使用了 redis 如果发生了缓存击穿、穿透、雪崩如何解决 redis 作为缓存， mysql的数据如何与redis进行同步？ 先介绍业务，看看业务符合什么场景\n一致性较高场景\n双写一致 读 写 | 延迟双删 分布式锁 读多写少 共享锁 排它锁 允许延迟一致\n介绍一下你的异步方案 允许延时一致的业务，采用异步通知 MQ消息中间件 Canal中间件 强一致性（Redisson读写锁） 共享锁 阻塞写操作 排它锁 阻塞读和写操作 Redis作为缓存，数据的持久化是怎么做的？ RDB save bgsave AOF RDB 的执行原理？ fork主进程得到子进程 子进程共享主进程的内存数据（复制页表） copy-on-write AOF 的执行原理 配置项\nalways\neverysec\nno\n重写触发\nbgrewriteaof\nRDB 和 AOF 对比 持久化方式 数据完整性 文件大小 宕机恢复速度 数据恢复优先级 系统资源占用 使用场景 redis 的key过期了会立即删除吗 惰性删除 定期删除 SLOW（10hz） FAST Redis的过期策略：惰性删除+定期删除 配合使用\nRedis 数据淘汰策略 noeviction volatile-ttl allkeys-random volatile-random allkeys-lru volatile-lru allkeys-lfu volatile-lfu 数据库有1000万数据，redis只能缓存20万，如何保证redis中的数据都是热点数据？ 使用 allkeys-lru\nRedis内存用完了会发生什么？ Redis 实现的分布式锁 分布式锁的使用场景\n定时任务 抢券 Redis 实现分布式锁如何合理控制锁的有效时长 根据业务执行时间预估 给锁续期 Redisson 实现的分布式锁执行流程 加锁 SETNX 加锁成功 watchdog续期（releaseTime/3） 释放锁 通知 watchdog Redisson 的锁是否可以重入 利用 hash结构 记录线程id和重入次数 主从一致性 主机挂机，导致两个线程获取同一把锁 解决方式:红锁 在多个实例上创建锁 (n/2+1) 向下取整 缺点 实现复杂 性能差 运维繁琐 redis 的 AP 思想，可以保证最终一致 zookeeper 基于 CP，实现强一致性 Redis 的集群有哪些方案，知道吗？ 主从复制 哨兵模式 分片集群 说一说 Redis 的注册主从复制 主从全量同步 ","permalink":"https://lv-blog.pages.dev/posts/programming/for-interview/redis-interview-questions/","summary":"\u003ch2 id=\"你的项目的哪些场景使用了-redis\"\u003e你的项目的哪些场景使用了 redis\u003c/h2\u003e\n\u003ch2 id=\"如果发生了缓存击穿穿透雪崩如何解决\"\u003e如果发生了缓存击穿、穿透、雪崩如何解决\u003c/h2\u003e\n\u003ch2 id=\"redis-作为缓存-mysql的数据如何与redis进行同步\"\u003eredis 作为缓存， mysql的数据如何与redis进行同步？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e先介绍业务，看看业务符合什么场景\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e一致性较高场景\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e双写一致\n\u003cul\u003e\n\u003cli\u003e读\u003c/li\u003e\n\u003cli\u003e写 | 延迟双删\u003c/li\u003e\n\u003cli\u003e分布式锁\n\u003cul\u003e\n\u003cli\u003e读多写少\n\u003cul\u003e\n\u003cli\u003e共享锁\u003c/li\u003e\n\u003cli\u003e排它锁\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e允许延迟一致\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"介绍一下你的异步方案\"\u003e介绍一下你的异步方案\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e允许延时一致的业务，采用异步通知\n\u003cul\u003e\n\u003cli\u003eMQ消息中间件\u003c/li\u003e\n\u003cli\u003eCanal中间件\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e强一致性（Redisson读写锁）\n\u003cul\u003e\n\u003cli\u003e共享锁\n\u003cul\u003e\n\u003cli\u003e阻塞写操作\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e排它锁\n\u003cul\u003e\n\u003cli\u003e阻塞读和写操作\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"redis作为缓存数据的持久化是怎么做的\"\u003eRedis作为缓存，数据的持久化是怎么做的？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eRDB\n\u003cul\u003e\n\u003cli\u003esave\u003c/li\u003e\n\u003cli\u003ebgsave\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003eAOF\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"rdb-的执行原理\"\u003eRDB 的执行原理？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003efork主进程得到子进程\u003c/li\u003e\n\u003cli\u003e子进程共享主进程的内存数据（复制页表）\u003c/li\u003e\n\u003cli\u003ecopy-on-write\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"aof-的执行原理\"\u003eAOF 的执行原理\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e配置项\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003ealways\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eeverysec\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003eno\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e重写触发\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003ebgrewriteaof\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"rdb-和-aof-对比\"\u003eRDB 和 AOF 对比\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e持久化方式\u003c/li\u003e\n\u003cli\u003e数据完整性\u003c/li\u003e\n\u003cli\u003e文件大小\u003c/li\u003e\n\u003cli\u003e宕机恢复速度\u003c/li\u003e\n\u003cli\u003e数据恢复优先级\u003c/li\u003e\n\u003cli\u003e系统资源占用\u003c/li\u003e\n\u003cli\u003e使用场景\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"redis-的key过期了会立即删除吗\"\u003eredis 的key过期了会立即删除吗\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e惰性删除\u003c/li\u003e\n\u003cli\u003e定期删除\n\u003cul\u003e\n\u003cli\u003eSLOW（10hz）\u003c/li\u003e\n\u003cli\u003eFAST\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003eRedis的过期策略：惰性删除+定期删除 配合使用\u003c/p\u003e\n\u003ch2 id=\"redis-数据淘汰策略\"\u003eRedis 数据淘汰策略\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003enoeviction\u003c/li\u003e\n\u003cli\u003evolatile-ttl\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eallkeys-random\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003evolatile-random\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eallkeys-lru\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003evolatile-lru\u003c/li\u003e\n\u003cli\u003eallkeys-lfu\u003c/li\u003e\n\u003cli\u003evolatile-lfu\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"数据库有1000万数据redis只能缓存20万如何保证redis中的数据都是热点数据\"\u003e数据库有1000万数据，redis只能缓存20万，如何保证redis中的数据都是热点数据？\u003c/h2\u003e\n\u003cp\u003e使用 allkeys-lru\u003c/p\u003e","title":"Redis 相关面试题整理"},{"content":" 从互联网整理而来\n一、JS/TS 前置基础 var、let、const 区别 原型、原型链原理 闭包概念、使用场景、内存泄漏风险 同步异步、宏任务微任务执行顺序 Promise 三种状态、链式调用、静态方法 async/await 底层原理 数组常用遍历方法差异 深浅拷贝实现与区别 Object.defineProperty 与 Proxy 对比 Reflect 对象作用 箭头函数与普通函数区别 解构赋值、展开运算符 ES6 模块 import/export 与 CommonJS 区别 TypeScript 泛型、接口、类型推导 EventLoop 完整执行流程 二、Vue2 基础核心 Vue 双向绑定底层原理 Object.defineProperty 如何监听对象、数组变化 Vue2 数组重写了哪些方法，为什么不能监听下标修改 Dep、Watcher、Observer 三者关系 什么是响应式依赖收集、派发更新 数据劫持流程完整过程 computed 和 watch 区别 computed 缓存实现原理 methods、computed、watch 使用场景区分 Vue 生命周期全部钩子及执行时机 created、mounted、updated、destroyed 使用场景 父子组件生命周期执行顺序 v-if 和 v-show 底层区别与性能差异 v-for 绑定 key 的作用，key 不能用 index 的原因 v-if 和 v-for 同时使用的优先级与问题 v-model 底层语法糖实现 修饰符 .lazy .number .trim 原理 指令自定义实现，钩子函数有哪些 过滤器 filter 使用场景，Vue3 废弃原因 插值表达式、v-text、v-html 区别 Vue 模板编译流程 render 函数与 template 关系 VNode 虚拟 DOM 结构作用 Diff 算法执行流程 Vue2 Diff 同层比较、key 作用、头尾指针优化 patch 函数更新逻辑 nextTick 原理，微任务/宏任务降级策略 $nextTick 使用场景 三、Vue2 组件通信 父子组件 props 传递规则，单向数据流 props 类型校验、默认值、必填配置 $emit 子传父实现 .sync 修饰符作用，Vue3 移除原因 $parent、$children、$refs 获取组件/元素 provide / inject 使用场景与响应式缺陷 EventBus 事件总线实现、内存泄漏问题 Vuex 全局状态通信完整流程 $attrs、$listeners 跨层级透传 跨页面、多层级组件通信方案对比 四、Vuex 状态管理 Vuex 五大核心概念 state、getters、mutations、actions、modules mutations 必须同步、actions 异步原因 commit 和 dispatch 区别 getters 缓存特性 modules 模块划分，命名空间 namespaced 辅助函数 mapState/mapGetters/mapMutations/mapActions 使用 Vuex 持久化存储方案 Vuex 严格模式 strict 作用 多个组件同时修改同一 state 数据流 Vuex 底层响应式原理 五、Vue Router 2/3 SPA 单页面路由实现原理 hash 模式与 history 模式底层区别 history 模式后端 nginx 配置问题 路由导航守卫分类：全局、路由独享、组件内 导航守卫执行完整顺序 $route 和 $router 区别 路由传参 params 和 query 差异 params 刷新丢失参数解决方案 动态路由匹配、路由嵌套 路由懒加载实现与分包 addRoutes、addRoute 动态添加路由 路由元信息 meta 使用场景 路由缓存 keep-alive 原理 include/exclude/max 参数作用 activated、deactivated 生命周期钩子 路由跳转 push、replace、go 区别 登录鉴权全局路由拦截实现 六、Vue2 进阶知识点 keep-alive 缓存组件底层逻辑 异步组件、组件懒加载 组件递归实现菜单、树形结构 $once、$off、$on 事件方法 插槽分类：默认插槽、具名插槽、作用域插槽 作用域插槽数据传递原理 v-once 指令作用 Vue 模板编译生成 AST 抽象语法树 异步更新队列机制 页面大量数据渲染卡顿优化方案 Object.freeze 冻结数据对响应式的影响 $set 原理，什么场景必须使用 Vue2 响应式存在的缺陷 全局过滤器、全局指令、全局组件注册 mixin 混入原理、合并规则、冲突问题 七、Vue3 核心升级（Composition API） Vue3 相比 Vue2 性能提升点 Vue3 响应式改用 Proxy 优势 Proxy 对比 Object.defineProperty 解决了哪些痛点 Reflect 配合 Proxy 的作用 reactive、ref 区别与底层实现 ref 为什么基础类型需要包裹对象 toRef、toRefs 使用场景，解决的问题 computed 在 setup 中使用方式 watch、watchEffect 区别 watch 监听对象、数组深度监听配置 setup 执行时机，this 指向问题 setup 参数 props、context 内容 Composition API 对比 Options API 优势 script setup 语法糖带来的变化 script setup 中 defineProps、defineEmits、defineExpose 生命周期钩子组合式写法 onMounted/onUpdated 等 getCurrentInstance 获取组件实例 provide / inject 在 Vue3 中如何实现响应式 自定义 hooks 封装思路，复用逻辑 Vue3 生命周期完整列表，与 Vue2 对应关系 八、Vue3 模板、编译、Diff 优化 Vue3 模板编译优化：PatchFlags 标记 静态提升原理，减少 VNode 创建开销 缓存内联事件处理函数 Vue3 Diff 算法优化：最长递增子序列 Diff 对比流程相比 Vue2 改进点 Fragment 根节点多标签支持原理 Teleport 传送门组件底层实现 Suspense 异步组件加载处理 v-model 在 Vue3 语法变更，多 v-model 实现 .v-model 修饰符自定义处理 插槽 v-slot 简写语法变化 Vue3 移除 filter 过滤器替代方案 九、Pinia（Vue3 状态管理） Pinia 相比 Vuex 的优势 Pinia 核心概念 state、getters、actions Pinia 支持同步异步，不再区分 mutations defineStore 定义仓库，id 作用 直接修改 state 与 $patch 批量更新区别 $reset、$subscribe、$onAction 使用场景 Pinia 模块化拆分，无需命名空间 Pinia 持久化插件使用 setup 语法下使用 Pinia 多个仓库互相调用数据方法 十、Vue Router 4（Vue3） createRouter、createWebHashHistory、createWebHistory RouterView、RouterLink v-slot 插槽用法 useRoute、useRouter 组合式 API 获取路由实例 动态路由 addRoute 替换 addRoutes 路由守卫组合式写法 路由缓存与 Suspense 配合 路由元类型 TS 类型提示处理 十一、Vue3 高级特性 watchPostEffect、watchSyncEffect 区别 shallowRef、shallowReactive 浅层响应式作用 readonly、shallowReadonly 只读代理 customRef 自定义响应式引用 effectScope 作用域收集副作用 defineCustomElement 构建原生 webComponent Vue3 全局 API 变更 createApp 替代 new Vue() app.config 全局配置项 全局组件、指令、插件注册方式变化 Transition、TransitionGroup 动画组件原理 v-memo 缓存 VNode 优化渲染 异步 setup 函数配合 Suspense 十二、工程化 \u0026amp; 构建工具 Vue CLI 与 Vite 底层构建区别 Vite 冷启动快的原理，esbuild 预构建 Vite 热更新 HMR 实现机制 vite.config.js 常用配置 webpack 打包 Vue 文件流程 vue-loader .vue SFC 单文件组件解析过程 scoped 样式隔离原理，深度选择器 /deep/ ::v-deep :deep() CSS Module 使用方式 环境变量 .env 文件区分开发测试生产 跨域代理 proxy 配置 打包体积优化方案 分包、懒加载、gzip 压缩配置 TS 在 Vue 项目中配置声明文件 第三方 UI 库按需引入配置 全局自动导入组件、API 插件 unplugin-auto-import 十三、网络请求与封装 axios 拦截器请求、响应封装思路 请求 loading、统一错误处理封装 token 过期自动刷新逻辑 请求取消、重复请求拦截方案 并发请求处理 接口数据统一格式化处理 封装请求 hooks 适配 Vue3 十四、性能优化专项 减少不必要组件渲染手段 v-once、v-memo 使用场景 长列表虚拟滚动实现原理 响应式数据避免不必要监听 computed 缓存减少重复计算 图片懒加载方案 路由懒加载分包优化 keep-alive 合理缓存减少重复请求 避免频繁触发 watch 大列表数据冻结 Object.freeze 减少 DOM 操作，合理使用虚拟 DOM 打包体积优化：Tree-Shaking、剔除无用依赖 Vite/Webpack 构建速度优化 内存泄漏常见场景：定时器、事件监听、未销毁订阅 Chrome Performance 面板排查页面卡顿 十五、混合、插件、拓展开发 Vue2 mixin 缺陷，Vue3 用 hooks 替代原因 Vue 插件开发完整结构 install 方法 全局自定义指令完整开发 封装业务通用组件规范 二次封装 Element Plus / Ant Design Vue 封装全局弹窗、消息提示组件 自定义全局权限指令 v-permission 页面权限、按钮权限实现方案 全局错误捕获 errorHandler 埋点、全局请求监控插件封装 十六、项目实战场景问答题 项目整体架构分层设计 Vue2 升级 Vue3 迁移改造遇到的问题 大型项目状态管理选型 Vuex / Pinia 理由 复杂树形菜单、表格组件封装思路 表单校验全局统一封装方案 页面权限控制完整实现流程 多环境配置、多渠道打包处理 首屏加载速度优化做了哪些处理 路由鉴权、未登录拦截逻辑 列表大量数据渲染卡顿如何解决 项目打包体积过大优化方案 页面缓存、返回保留表单状态实现 接口并发冲突、重复提交拦截 移动端适配方案 vw/vh、rem 线上报错监控、异常捕获方案 ","permalink":"https://lv-blog.pages.dev/posts/programming/frontend/vue-interview-questions/","summary":"\u003cblockquote\u003e\n\u003cp\u003e从互联网整理而来\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"一jsts-前置基础\"\u003e一、JS/TS 前置基础\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003evar、let、const 区别\u003c/li\u003e\n\u003cli\u003e原型、原型链原理\u003c/li\u003e\n\u003cli\u003e闭包概念、使用场景、内存泄漏风险\u003c/li\u003e\n\u003cli\u003e同步异步、宏任务微任务执行顺序\u003c/li\u003e\n\u003cli\u003ePromise 三种状态、链式调用、静态方法\u003c/li\u003e\n\u003cli\u003easync/await 底层原理\u003c/li\u003e\n\u003cli\u003e数组常用遍历方法差异\u003c/li\u003e\n\u003cli\u003e深浅拷贝实现与区别\u003c/li\u003e\n\u003cli\u003eObject.defineProperty 与 Proxy 对比\u003c/li\u003e\n\u003cli\u003eReflect 对象作用\u003c/li\u003e\n\u003cli\u003e箭头函数与普通函数区别\u003c/li\u003e\n\u003cli\u003e解构赋值、展开运算符\u003c/li\u003e\n\u003cli\u003eES6 模块 import/export 与 CommonJS 区别\u003c/li\u003e\n\u003cli\u003eTypeScript 泛型、接口、类型推导\u003c/li\u003e\n\u003cli\u003eEventLoop 完整执行流程\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"二vue2-基础核心\"\u003e二、Vue2 基础核心\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eVue 双向绑定底层原理\u003c/li\u003e\n\u003cli\u003eObject.defineProperty 如何监听对象、数组变化\u003c/li\u003e\n\u003cli\u003eVue2 数组重写了哪些方法，为什么不能监听下标修改\u003c/li\u003e\n\u003cli\u003eDep、Watcher、Observer 三者关系\u003c/li\u003e\n\u003cli\u003e什么是响应式依赖收集、派发更新\u003c/li\u003e\n\u003cli\u003e数据劫持流程完整过程\u003c/li\u003e\n\u003cli\u003ecomputed 和 watch 区别\u003c/li\u003e\n\u003cli\u003ecomputed 缓存实现原理\u003c/li\u003e\n\u003cli\u003emethods、computed、watch 使用场景区分\u003c/li\u003e\n\u003cli\u003eVue 生命周期全部钩子及执行时机\u003c/li\u003e\n\u003cli\u003ecreated、mounted、updated、destroyed 使用场景\u003c/li\u003e\n\u003cli\u003e父子组件生命周期执行顺序\u003c/li\u003e\n\u003cli\u003ev-if 和 v-show 底层区别与性能差异\u003c/li\u003e\n\u003cli\u003ev-for 绑定 key 的作用，key 不能用 index 的原因\u003c/li\u003e\n\u003cli\u003ev-if 和 v-for 同时使用的优先级与问题\u003c/li\u003e\n\u003cli\u003ev-model 底层语法糖实现\u003c/li\u003e\n\u003cli\u003e修饰符 .lazy .number .trim 原理\u003c/li\u003e\n\u003cli\u003e指令自定义实现，钩子函数有哪些\u003c/li\u003e\n\u003cli\u003e过滤器 filter 使用场景，Vue3 废弃原因\u003c/li\u003e\n\u003cli\u003e插值表达式、v-text、v-html 区别\u003c/li\u003e\n\u003cli\u003eVue 模板编译流程\u003c/li\u003e\n\u003cli\u003erender 函数与 template 关系\u003c/li\u003e\n\u003cli\u003eVNode 虚拟 DOM 结构作用\u003c/li\u003e\n\u003cli\u003eDiff 算法执行流程\u003c/li\u003e\n\u003cli\u003eVue2 Diff 同层比较、key 作用、头尾指针优化\u003c/li\u003e\n\u003cli\u003epatch 函数更新逻辑\u003c/li\u003e\n\u003cli\u003enextTick 原理，微任务/宏任务降级策略\u003c/li\u003e\n\u003cli\u003e$nextTick 使用场景\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"三vue2-组件通信\"\u003e三、Vue2 组件通信\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e父子组件 props 传递规则，单向数据流\u003c/li\u003e\n\u003cli\u003eprops 类型校验、默认值、必填配置\u003c/li\u003e\n\u003cli\u003e$emit 子传父实现\u003c/li\u003e\n\u003cli\u003e.sync 修饰符作用，Vue3 移除原因\u003c/li\u003e\n\u003cli\u003e$parent、$children、$refs 获取组件/元素\u003c/li\u003e\n\u003cli\u003eprovide / inject 使用场景与响应式缺陷\u003c/li\u003e\n\u003cli\u003eEventBus 事件总线实现、内存泄漏问题\u003c/li\u003e\n\u003cli\u003eVuex 全局状态通信完整流程\u003c/li\u003e\n\u003cli\u003e$attrs、$listeners 跨层级透传\u003c/li\u003e\n\u003cli\u003e跨页面、多层级组件通信方案对比\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"四vuex-状态管理\"\u003e四、Vuex 状态管理\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eVuex 五大核心概念 state、getters、mutations、actions、modules\u003c/li\u003e\n\u003cli\u003emutations 必须同步、actions 异步原因\u003c/li\u003e\n\u003cli\u003ecommit 和 dispatch 区别\u003c/li\u003e\n\u003cli\u003egetters 缓存特性\u003c/li\u003e\n\u003cli\u003emodules 模块划分，命名空间 namespaced\u003c/li\u003e\n\u003cli\u003e辅助函数 mapState/mapGetters/mapMutations/mapActions 使用\u003c/li\u003e\n\u003cli\u003eVuex 持久化存储方案\u003c/li\u003e\n\u003cli\u003eVuex 严格模式 strict 作用\u003c/li\u003e\n\u003cli\u003e多个组件同时修改同一 state 数据流\u003c/li\u003e\n\u003cli\u003eVuex 底层响应式原理\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"五vue-router-23\"\u003e五、Vue Router 2/3\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eSPA 单页面路由实现原理\u003c/li\u003e\n\u003cli\u003ehash 模式与 history 模式底层区别\u003c/li\u003e\n\u003cli\u003ehistory 模式后端 nginx 配置问题\u003c/li\u003e\n\u003cli\u003e路由导航守卫分类：全局、路由独享、组件内\u003c/li\u003e\n\u003cli\u003e导航守卫执行完整顺序\u003c/li\u003e\n\u003cli\u003e$route 和 $router 区别\u003c/li\u003e\n\u003cli\u003e路由传参 params 和 query 差异\u003c/li\u003e\n\u003cli\u003eparams 刷新丢失参数解决方案\u003c/li\u003e\n\u003cli\u003e动态路由匹配、路由嵌套\u003c/li\u003e\n\u003cli\u003e路由懒加载实现与分包\u003c/li\u003e\n\u003cli\u003eaddRoutes、addRoute 动态添加路由\u003c/li\u003e\n\u003cli\u003e路由元信息 meta 使用场景\u003c/li\u003e\n\u003cli\u003e路由缓存 keep-alive 原理\u003c/li\u003e\n\u003cli\u003einclude/exclude/max 参数作用\u003c/li\u003e\n\u003cli\u003eactivated、deactivated 生命周期钩子\u003c/li\u003e\n\u003cli\u003e路由跳转 push、replace、go 区别\u003c/li\u003e\n\u003cli\u003e登录鉴权全局路由拦截实现\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"六vue2-进阶知识点\"\u003e六、Vue2 进阶知识点\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003ekeep-alive 缓存组件底层逻辑\u003c/li\u003e\n\u003cli\u003e异步组件、组件懒加载\u003c/li\u003e\n\u003cli\u003e组件递归实现菜单、树形结构\u003c/li\u003e\n\u003cli\u003e$once、$off、$on 事件方法\u003c/li\u003e\n\u003cli\u003e插槽分类：默认插槽、具名插槽、作用域插槽\u003c/li\u003e\n\u003cli\u003e作用域插槽数据传递原理\u003c/li\u003e\n\u003cli\u003ev-once 指令作用\u003c/li\u003e\n\u003cli\u003eVue 模板编译生成 AST 抽象语法树\u003c/li\u003e\n\u003cli\u003e异步更新队列机制\u003c/li\u003e\n\u003cli\u003e页面大量数据渲染卡顿优化方案\u003c/li\u003e\n\u003cli\u003eObject.freeze 冻结数据对响应式的影响\u003c/li\u003e\n\u003cli\u003e$set 原理，什么场景必须使用\u003c/li\u003e\n\u003cli\u003eVue2 响应式存在的缺陷\u003c/li\u003e\n\u003cli\u003e全局过滤器、全局指令、全局组件注册\u003c/li\u003e\n\u003cli\u003emixin 混入原理、合并规则、冲突问题\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"七vue3-核心升级composition-api\"\u003e七、Vue3 核心升级（Composition API）\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eVue3 相比 Vue2 性能提升点\u003c/li\u003e\n\u003cli\u003eVue3 响应式改用 Proxy 优势\u003c/li\u003e\n\u003cli\u003eProxy 对比 Object.defineProperty 解决了哪些痛点\u003c/li\u003e\n\u003cli\u003eReflect 配合 Proxy 的作用\u003c/li\u003e\n\u003cli\u003ereactive、ref 区别与底层实现\u003c/li\u003e\n\u003cli\u003eref 为什么基础类型需要包裹对象\u003c/li\u003e\n\u003cli\u003etoRef、toRefs 使用场景，解决的问题\u003c/li\u003e\n\u003cli\u003ecomputed 在 setup 中使用方式\u003c/li\u003e\n\u003cli\u003ewatch、watchEffect 区别\u003c/li\u003e\n\u003cli\u003ewatch 监听对象、数组深度监听配置\u003c/li\u003e\n\u003cli\u003esetup 执行时机，this 指向问题\u003c/li\u003e\n\u003cli\u003esetup 参数 props、context 内容\u003c/li\u003e\n\u003cli\u003eComposition API 对比 Options API 优势\u003c/li\u003e\n\u003cli\u003escript setup 语法糖带来的变化\u003c/li\u003e\n\u003cli\u003escript setup 中 defineProps、defineEmits、defineExpose\u003c/li\u003e\n\u003cli\u003e生命周期钩子组合式写法 onMounted/onUpdated 等\u003c/li\u003e\n\u003cli\u003egetCurrentInstance 获取组件实例\u003c/li\u003e\n\u003cli\u003eprovide / inject 在 Vue3 中如何实现响应式\u003c/li\u003e\n\u003cli\u003e自定义 hooks 封装思路，复用逻辑\u003c/li\u003e\n\u003cli\u003eVue3 生命周期完整列表，与 Vue2 对应关系\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"八vue3-模板编译diff-优化\"\u003e八、Vue3 模板、编译、Diff 优化\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eVue3 模板编译优化：PatchFlags 标记\u003c/li\u003e\n\u003cli\u003e静态提升原理，减少 VNode 创建开销\u003c/li\u003e\n\u003cli\u003e缓存内联事件处理函数\u003c/li\u003e\n\u003cli\u003eVue3 Diff 算法优化：最长递增子序列\u003c/li\u003e\n\u003cli\u003eDiff 对比流程相比 Vue2 改进点\u003c/li\u003e\n\u003cli\u003eFragment 根节点多标签支持原理\u003c/li\u003e\n\u003cli\u003eTeleport 传送门组件底层实现\u003c/li\u003e\n\u003cli\u003eSuspense 异步组件加载处理\u003c/li\u003e\n\u003cli\u003ev-model 在 Vue3 语法变更，多 v-model 实现\u003c/li\u003e\n\u003cli\u003e.v-model 修饰符自定义处理\u003c/li\u003e\n\u003cli\u003e插槽 v-slot 简写语法变化\u003c/li\u003e\n\u003cli\u003eVue3 移除 filter 过滤器替代方案\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"九piniavue3-状态管理\"\u003e九、Pinia（Vue3 状态管理）\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003ePinia 相比 Vuex 的优势\u003c/li\u003e\n\u003cli\u003ePinia 核心概念 state、getters、actions\u003c/li\u003e\n\u003cli\u003ePinia 支持同步异步，不再区分 mutations\u003c/li\u003e\n\u003cli\u003edefineStore 定义仓库，id 作用\u003c/li\u003e\n\u003cli\u003e直接修改 state 与 $patch 批量更新区别\u003c/li\u003e\n\u003cli\u003e$reset、$subscribe、$onAction 使用场景\u003c/li\u003e\n\u003cli\u003ePinia 模块化拆分，无需命名空间\u003c/li\u003e\n\u003cli\u003ePinia 持久化插件使用\u003c/li\u003e\n\u003cli\u003esetup 语法下使用 Pinia\u003c/li\u003e\n\u003cli\u003e多个仓库互相调用数据方法\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十vue-router-4vue3\"\u003e十、Vue Router 4（Vue3）\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003ecreateRouter、createWebHashHistory、createWebHistory\u003c/li\u003e\n\u003cli\u003eRouterView、RouterLink v-slot 插槽用法\u003c/li\u003e\n\u003cli\u003euseRoute、useRouter 组合式 API 获取路由实例\u003c/li\u003e\n\u003cli\u003e动态路由 addRoute 替换 addRoutes\u003c/li\u003e\n\u003cli\u003e路由守卫组合式写法\u003c/li\u003e\n\u003cli\u003e路由缓存与 Suspense 配合\u003c/li\u003e\n\u003cli\u003e路由元类型 TS 类型提示处理\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十一vue3-高级特性\"\u003e十一、Vue3 高级特性\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003ewatchPostEffect、watchSyncEffect 区别\u003c/li\u003e\n\u003cli\u003eshallowRef、shallowReactive 浅层响应式作用\u003c/li\u003e\n\u003cli\u003ereadonly、shallowReadonly 只读代理\u003c/li\u003e\n\u003cli\u003ecustomRef 自定义响应式引用\u003c/li\u003e\n\u003cli\u003eeffectScope 作用域收集副作用\u003c/li\u003e\n\u003cli\u003edefineCustomElement 构建原生 webComponent\u003c/li\u003e\n\u003cli\u003eVue3 全局 API 变更 createApp 替代 new Vue()\u003c/li\u003e\n\u003cli\u003eapp.config 全局配置项\u003c/li\u003e\n\u003cli\u003e全局组件、指令、插件注册方式变化\u003c/li\u003e\n\u003cli\u003eTransition、TransitionGroup 动画组件原理\u003c/li\u003e\n\u003cli\u003ev-memo 缓存 VNode 优化渲染\u003c/li\u003e\n\u003cli\u003e异步 setup 函数配合 Suspense\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十二工程化--构建工具\"\u003e十二、工程化 \u0026amp; 构建工具\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eVue CLI 与 Vite 底层构建区别\u003c/li\u003e\n\u003cli\u003eVite 冷启动快的原理，esbuild 预构建\u003c/li\u003e\n\u003cli\u003eVite 热更新 HMR 实现机制\u003c/li\u003e\n\u003cli\u003evite.config.js 常用配置\u003c/li\u003e\n\u003cli\u003ewebpack 打包 Vue 文件流程 vue-loader\u003c/li\u003e\n\u003cli\u003e.vue SFC 单文件组件解析过程\u003c/li\u003e\n\u003cli\u003escoped 样式隔离原理，深度选择器 /deep/ ::v-deep :deep()\u003c/li\u003e\n\u003cli\u003eCSS Module 使用方式\u003c/li\u003e\n\u003cli\u003e环境变量 .env 文件区分开发测试生产\u003c/li\u003e\n\u003cli\u003e跨域代理 proxy 配置\u003c/li\u003e\n\u003cli\u003e打包体积优化方案\u003c/li\u003e\n\u003cli\u003e分包、懒加载、gzip 压缩配置\u003c/li\u003e\n\u003cli\u003eTS 在 Vue 项目中配置声明文件\u003c/li\u003e\n\u003cli\u003e第三方 UI 库按需引入配置\u003c/li\u003e\n\u003cli\u003e全局自动导入组件、API 插件 unplugin-auto-import\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十三网络请求与封装\"\u003e十三、网络请求与封装\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eaxios 拦截器请求、响应封装思路\u003c/li\u003e\n\u003cli\u003e请求 loading、统一错误处理封装\u003c/li\u003e\n\u003cli\u003etoken 过期自动刷新逻辑\u003c/li\u003e\n\u003cli\u003e请求取消、重复请求拦截方案\u003c/li\u003e\n\u003cli\u003e并发请求处理\u003c/li\u003e\n\u003cli\u003e接口数据统一格式化处理\u003c/li\u003e\n\u003cli\u003e封装请求 hooks 适配 Vue3\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十四性能优化专项\"\u003e十四、性能优化专项\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e减少不必要组件渲染手段\u003c/li\u003e\n\u003cli\u003ev-once、v-memo 使用场景\u003c/li\u003e\n\u003cli\u003e长列表虚拟滚动实现原理\u003c/li\u003e\n\u003cli\u003e响应式数据避免不必要监听\u003c/li\u003e\n\u003cli\u003ecomputed 缓存减少重复计算\u003c/li\u003e\n\u003cli\u003e图片懒加载方案\u003c/li\u003e\n\u003cli\u003e路由懒加载分包优化\u003c/li\u003e\n\u003cli\u003ekeep-alive 合理缓存减少重复请求\u003c/li\u003e\n\u003cli\u003e避免频繁触发 watch\u003c/li\u003e\n\u003cli\u003e大列表数据冻结 Object.freeze\u003c/li\u003e\n\u003cli\u003e减少 DOM 操作，合理使用虚拟 DOM\u003c/li\u003e\n\u003cli\u003e打包体积优化：Tree-Shaking、剔除无用依赖\u003c/li\u003e\n\u003cli\u003eVite/Webpack 构建速度优化\u003c/li\u003e\n\u003cli\u003e内存泄漏常见场景：定时器、事件监听、未销毁订阅\u003c/li\u003e\n\u003cli\u003eChrome Performance 面板排查页面卡顿\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十五混合插件拓展开发\"\u003e十五、混合、插件、拓展开发\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eVue2 mixin 缺陷，Vue3 用 hooks 替代原因\u003c/li\u003e\n\u003cli\u003eVue 插件开发完整结构 install 方法\u003c/li\u003e\n\u003cli\u003e全局自定义指令完整开发\u003c/li\u003e\n\u003cli\u003e封装业务通用组件规范\u003c/li\u003e\n\u003cli\u003e二次封装 Element Plus / Ant Design Vue\u003c/li\u003e\n\u003cli\u003e封装全局弹窗、消息提示组件\u003c/li\u003e\n\u003cli\u003e自定义全局权限指令 v-permission\u003c/li\u003e\n\u003cli\u003e页面权限、按钮权限实现方案\u003c/li\u003e\n\u003cli\u003e全局错误捕获 errorHandler\u003c/li\u003e\n\u003cli\u003e埋点、全局请求监控插件封装\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十六项目实战场景问答题\"\u003e十六、项目实战场景问答题\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e项目整体架构分层设计\u003c/li\u003e\n\u003cli\u003eVue2 升级 Vue3 迁移改造遇到的问题\u003c/li\u003e\n\u003cli\u003e大型项目状态管理选型 Vuex / Pinia 理由\u003c/li\u003e\n\u003cli\u003e复杂树形菜单、表格组件封装思路\u003c/li\u003e\n\u003cli\u003e表单校验全局统一封装方案\u003c/li\u003e\n\u003cli\u003e页面权限控制完整实现流程\u003c/li\u003e\n\u003cli\u003e多环境配置、多渠道打包处理\u003c/li\u003e\n\u003cli\u003e首屏加载速度优化做了哪些处理\u003c/li\u003e\n\u003cli\u003e路由鉴权、未登录拦截逻辑\u003c/li\u003e\n\u003cli\u003e列表大量数据渲染卡顿如何解决\u003c/li\u003e\n\u003cli\u003e项目打包体积过大优化方案\u003c/li\u003e\n\u003cli\u003e页面缓存、返回保留表单状态实现\u003c/li\u003e\n\u003cli\u003e接口并发冲突、重复提交拦截\u003c/li\u003e\n\u003cli\u003e移动端适配方案 vw/vh、rem\u003c/li\u003e\n\u003cli\u003e线上报错监控、异常捕获方案\u003c/li\u003e\n\u003c/ol\u003e","title":"Vue 相关面试题整理"},{"content":" 从互联网整理而来\n一、基础概念与运行机制 小程序与H5、App的核心区别 小程序双线程模型分别是什么，各自职责 逻辑层与视图层通信方式 小程序渲染流程完整过程 小程序启动流程（冷启动、热启动） 冷启动和热启动的差异 小程序分包加载机制概念 主包与分包限制规则 独立分包和普通分包区别 小程序的生命周期分类（应用、页面、组件） 小程序页面栈规则，最多存放多少页面 小程序登录流程整体逻辑 微信授权两种类型区别（基础授权、隐私授权） 小程序原生宿主环境提供哪些能力 小程序更新机制，静默更新与主动更新 二、文件结构与配置 小程序四种核心文件作用（json/wxml/wxss/js） app.json 全局配置常用字段 page.json 页面配置优先级与app.json关系 sitemap.json 作用 project.config.json 用途 app.js 全局App实例生命周期 页面js Page构造器生命周期 组件js Component构造器生命周期 usingComponents 引入组件配置 全局样式app.wxss和页面wxss权重规则 wxss与css差异，特有特性 rpx单位换算规则，适配原理 小程序支持的选择器有哪些，不支持哪些 图片、静态资源本地与线上引入规则 分包配置subpackages字段参数含义 三、WXML 模板语法 WXML数据绑定语法规则 插值表达式支持哪些运算 wx:if、wx:elif、wx:else 条件渲染 hidden属性与wx:if性能差异 wx:for列表渲染，wx:key作用 wx:key使用规范，禁止index的场景 block标签作用，是否生成真实节点 模板template定义与引入，数据传递 wxs脚本作用，运行环境限制 wxs与js的区别，能否调用小程序API 事件绑定bind、catch、capture-bind区别 mut-bind互斥事件作用 事件传参几种实现方式 数据双向绑定model语法 标签属性布尔值绑定规则 四、组件系统（原生+自定义组件） 小程序自定义组件创建完整结构 Component中properties传参配置规则 properties三种数据监听方式 data、properties、observers执行顺序 observers监听对象、数组深层变化写法 组件生命周期完整钩子 lifetimes与pageLifetimes区别 组件插槽slot，具名插槽使用 组件父子通信方式 triggerEvent触发自定义事件传参 selectComponent、selectAllComponents获取子组件 behaviors复用逻辑，合并规则 原生内置常用容器、表单、媒体组件 scroll-view滚动注意事项 movable-area、movable-view实现拖拽原理 canvas新旧版本差异，2d画布使用 web-view嵌入H5通信方案 cover-view、cover-image使用限制场景 自定义组件样式隔离options配置 组件pureDataPattern作用 五、数据管理与渲染更新 setData底层更新原理 setData频繁调用性能问题 setData单次数据大小限制 局部更新数据简写写法 直接修改this.data不调用setData为什么不更新视图 复杂对象、数组更新setData写法 全局数据共享几种方案 getApp()全局变量优缺点 globalData跨页面同步更新缺陷 数据本地持久化storage存储限制 wx.setStorage同步异步API区别 storage大数据存储优化方案 页面间数据传递方式对比 页面栈取上一页实例方法 大数据列表渲染卡顿优化思路 六、路由与页面跳转 五种路由API区别（navigateTo/redirectTo/reLaunch/switchTab/navigateBack） switchTab跳转限制条件 navigateTo无法跳tab页面解决方案 路由传参两种方式，长参数丢失问题 页面返回监听onUnload、onShow触发时机 获取当前页面栈方法getCurrentPages() 路由拦截统一封装思路 tabBar配置规则，自定义tabBar实现 自定义tabBar踩坑点 页面跳转后保留表单状态方案 七、网络请求与后端交互 wx.request请求基础配置 request合法域名校验规则，开发环境规避 小程序请求携带cookie机制 请求header统一封装token思路 拦截器封装wx.request方案 并发请求、请求超时处理 文件上传wx.uploadFile参数配置 文件下载wx.downloadFile使用场景 websocket实时通信完整流程 socket断线重连实现逻辑 接口返回加密数据解密处理 批量请求统一捕获异常 小程序不支持跨域的根本原因 上传图片压缩、裁剪处理 上传进度监听API 八、登录、授权、用户信息 wx.login获取code作用 code换取openid完整流程 openid、unionid区别与生成规则 隐私保护新规下获取用户头像昵称方案 getUserInfo接口废弃替代方案 手机号一键登录流程 授权scope各权限含义 用户拒绝授权后重新引导授权逻辑 登录态session_key过期处理 第三方登录、关联账号实现 登录状态持久化存储方案 多端打通unionid获取条件 隐私协议弹窗实现规范 小程序订阅消息下发流程 模板消息与订阅消息区别 九、媒体、图片、地图、设备API wx.chooseMedia选择图片视频参数 图片预览wx.previewImage使用 图片本地缓存方案 录音、播放音频API使用 video视频播放常见问题 map地图组件标记、覆盖物绘制 获取当前定位wx.getLocation权限配置 后台持续定位配置要求 设备信息、系统API获取 振动、剪贴板、屏幕亮度API 扫码wx.scanCode场景 蓝牙BLE设备连接流程 NFC基础使用场景 相机camera组件适配 文件管理FileSystemManager读写文件 十、性能优化专项 setData性能优化手段 长列表渲染优化方案 虚拟列表实现思路 分包加载优化首屏速度 图片懒加载处理 减少页面、组件层级嵌套 wxss样式体积压缩优化 分包预加载preloadRule配置 避免频繁创建定时器、监听事件 页面卸载清除所有订阅、定时器 包体积超限优化方案 开发者工具性能面板排查卡顿 避免在onShow频繁请求接口 图片资源压缩、webp适配 减少observers深层监听开销 十一、适配、兼容、多端 rpx、px、rem转换关系 刘海屏、安全区适配css写法 深色模式适配配置 不同微信版本API兼容处理 caniuse判断接口是否可用 小程序转H5、转uni-app差异 平板、PC端小程序适配要点 低版本微信兼容降级方案 字体、图片多设备适配 自定义导航栏适配胶囊按钮 十二、工程化、第三方框架 原生小程序、uni-app、Taro区别 Taro编译原理，多端适配逻辑 uni-app分包、条件编译写法 小程序npm依赖使用规则 按需引入UI组件库优化 工程化打包、环境变量区分 多环境请求域名配置 ESLint校验小程序代码规范 Git协同开发规范 CI自动上传小程序工具 十三、支付、分享、广告、业务能力 小程序支付完整流程 支付回调后端处理逻辑 分享onShareAppMessage、onShareTimeline配置 分享图、分享路径动态处理 分享到朋友圈限制条件 插屏广告、激励视频广告接入 小程序跳转其他小程序API 跳转限制与关联配置要求 客服消息接入流程 商品卡片、搜索收录配置sitemap 十四、安全、审核、合规 小程序合法域名、业务域名、web-view域名区别 前端存储敏感信息安全风险 session_key不能传输到前端原因 接口数据防篡改简单方案 隐私合规要求有哪些 图片、内容审核API调用 小程序常见审核驳回原因 诱导分享、诱导支付违规点 本地存储数据加密思路 防止恶意刷接口方案 十五、实战场景综合题 项目整体目录架构设计 封装通用请求库完整思路 全局登录拦截、未登录跳转实现 大型表单组件封装与校验 多页面共享登录状态管理 列表上拉加载、下拉刷新封装 图片上传、预览、删除完整流程 分包改造解决主包体积超限 页面返回保留搜索条件、分页数据 小程序热更新主动检测逻辑 websocket聊天断线重连方案 自定义tabBar遇到的坑与解决 页面大量数据滑动卡顿优化 线上白屏、加载失败排查思路 原生小程序迁移Taro/uni改造方案 ","permalink":"https://lv-blog.pages.dev/posts/programming/frontend/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F%E7%9B%B8%E5%85%B3%E9%9D%A2%E8%AF%95%E9%A2%98/","summary":"\u003cblockquote\u003e\n\u003cp\u003e从互联网整理而来\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"一基础概念与运行机制\"\u003e一、基础概念与运行机制\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e小程序与H5、App的核心区别\u003c/li\u003e\n\u003cli\u003e小程序双线程模型分别是什么，各自职责\u003c/li\u003e\n\u003cli\u003e逻辑层与视图层通信方式\u003c/li\u003e\n\u003cli\u003e小程序渲染流程完整过程\u003c/li\u003e\n\u003cli\u003e小程序启动流程（冷启动、热启动）\u003c/li\u003e\n\u003cli\u003e冷启动和热启动的差异\u003c/li\u003e\n\u003cli\u003e小程序分包加载机制概念\u003c/li\u003e\n\u003cli\u003e主包与分包限制规则\u003c/li\u003e\n\u003cli\u003e独立分包和普通分包区别\u003c/li\u003e\n\u003cli\u003e小程序的生命周期分类（应用、页面、组件）\u003c/li\u003e\n\u003cli\u003e小程序页面栈规则，最多存放多少页面\u003c/li\u003e\n\u003cli\u003e小程序登录流程整体逻辑\u003c/li\u003e\n\u003cli\u003e微信授权两种类型区别（基础授权、隐私授权）\u003c/li\u003e\n\u003cli\u003e小程序原生宿主环境提供哪些能力\u003c/li\u003e\n\u003cli\u003e小程序更新机制，静默更新与主动更新\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"二文件结构与配置\"\u003e二、文件结构与配置\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e小程序四种核心文件作用（json/wxml/wxss/js）\u003c/li\u003e\n\u003cli\u003eapp.json 全局配置常用字段\u003c/li\u003e\n\u003cli\u003epage.json 页面配置优先级与app.json关系\u003c/li\u003e\n\u003cli\u003esitemap.json 作用\u003c/li\u003e\n\u003cli\u003eproject.config.json 用途\u003c/li\u003e\n\u003cli\u003eapp.js 全局App实例生命周期\u003c/li\u003e\n\u003cli\u003e页面js Page构造器生命周期\u003c/li\u003e\n\u003cli\u003e组件js Component构造器生命周期\u003c/li\u003e\n\u003cli\u003eusingComponents 引入组件配置\u003c/li\u003e\n\u003cli\u003e全局样式app.wxss和页面wxss权重规则\u003c/li\u003e\n\u003cli\u003ewxss与css差异，特有特性\u003c/li\u003e\n\u003cli\u003erpx单位换算规则，适配原理\u003c/li\u003e\n\u003cli\u003e小程序支持的选择器有哪些，不支持哪些\u003c/li\u003e\n\u003cli\u003e图片、静态资源本地与线上引入规则\u003c/li\u003e\n\u003cli\u003e分包配置subpackages字段参数含义\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"三wxml-模板语法\"\u003e三、WXML 模板语法\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003eWXML数据绑定语法规则\u003c/li\u003e\n\u003cli\u003e插值表达式支持哪些运算\u003c/li\u003e\n\u003cli\u003ewx:if、wx:elif、wx:else 条件渲染\u003c/li\u003e\n\u003cli\u003ehidden属性与wx:if性能差异\u003c/li\u003e\n\u003cli\u003ewx:for列表渲染，wx:key作用\u003c/li\u003e\n\u003cli\u003ewx:key使用规范，禁止index的场景\u003c/li\u003e\n\u003cli\u003eblock标签作用，是否生成真实节点\u003c/li\u003e\n\u003cli\u003e模板template定义与引入，数据传递\u003c/li\u003e\n\u003cli\u003ewxs脚本作用，运行环境限制\u003c/li\u003e\n\u003cli\u003ewxs与js的区别，能否调用小程序API\u003c/li\u003e\n\u003cli\u003e事件绑定bind、catch、capture-bind区别\u003c/li\u003e\n\u003cli\u003emut-bind互斥事件作用\u003c/li\u003e\n\u003cli\u003e事件传参几种实现方式\u003c/li\u003e\n\u003cli\u003e数据双向绑定model语法\u003c/li\u003e\n\u003cli\u003e标签属性布尔值绑定规则\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"四组件系统原生自定义组件\"\u003e四、组件系统（原生+自定义组件）\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e小程序自定义组件创建完整结构\u003c/li\u003e\n\u003cli\u003eComponent中properties传参配置规则\u003c/li\u003e\n\u003cli\u003eproperties三种数据监听方式\u003c/li\u003e\n\u003cli\u003edata、properties、observers执行顺序\u003c/li\u003e\n\u003cli\u003eobservers监听对象、数组深层变化写法\u003c/li\u003e\n\u003cli\u003e组件生命周期完整钩子\u003c/li\u003e\n\u003cli\u003elifetimes与pageLifetimes区别\u003c/li\u003e\n\u003cli\u003e组件插槽slot，具名插槽使用\u003c/li\u003e\n\u003cli\u003e组件父子通信方式\u003c/li\u003e\n\u003cli\u003etriggerEvent触发自定义事件传参\u003c/li\u003e\n\u003cli\u003eselectComponent、selectAllComponents获取子组件\u003c/li\u003e\n\u003cli\u003ebehaviors复用逻辑，合并规则\u003c/li\u003e\n\u003cli\u003e原生内置常用容器、表单、媒体组件\u003c/li\u003e\n\u003cli\u003escroll-view滚动注意事项\u003c/li\u003e\n\u003cli\u003emovable-area、movable-view实现拖拽原理\u003c/li\u003e\n\u003cli\u003ecanvas新旧版本差异，2d画布使用\u003c/li\u003e\n\u003cli\u003eweb-view嵌入H5通信方案\u003c/li\u003e\n\u003cli\u003ecover-view、cover-image使用限制场景\u003c/li\u003e\n\u003cli\u003e自定义组件样式隔离options配置\u003c/li\u003e\n\u003cli\u003e组件pureDataPattern作用\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"五数据管理与渲染更新\"\u003e五、数据管理与渲染更新\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003esetData底层更新原理\u003c/li\u003e\n\u003cli\u003esetData频繁调用性能问题\u003c/li\u003e\n\u003cli\u003esetData单次数据大小限制\u003c/li\u003e\n\u003cli\u003e局部更新数据简写写法\u003c/li\u003e\n\u003cli\u003e直接修改this.data不调用setData为什么不更新视图\u003c/li\u003e\n\u003cli\u003e复杂对象、数组更新setData写法\u003c/li\u003e\n\u003cli\u003e全局数据共享几种方案\u003c/li\u003e\n\u003cli\u003egetApp()全局变量优缺点\u003c/li\u003e\n\u003cli\u003eglobalData跨页面同步更新缺陷\u003c/li\u003e\n\u003cli\u003e数据本地持久化storage存储限制\u003c/li\u003e\n\u003cli\u003ewx.setStorage同步异步API区别\u003c/li\u003e\n\u003cli\u003estorage大数据存储优化方案\u003c/li\u003e\n\u003cli\u003e页面间数据传递方式对比\u003c/li\u003e\n\u003cli\u003e页面栈取上一页实例方法\u003c/li\u003e\n\u003cli\u003e大数据列表渲染卡顿优化思路\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"六路由与页面跳转\"\u003e六、路由与页面跳转\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e五种路由API区别（navigateTo/redirectTo/reLaunch/switchTab/navigateBack）\u003c/li\u003e\n\u003cli\u003eswitchTab跳转限制条件\u003c/li\u003e\n\u003cli\u003enavigateTo无法跳tab页面解决方案\u003c/li\u003e\n\u003cli\u003e路由传参两种方式，长参数丢失问题\u003c/li\u003e\n\u003cli\u003e页面返回监听onUnload、onShow触发时机\u003c/li\u003e\n\u003cli\u003e获取当前页面栈方法getCurrentPages()\u003c/li\u003e\n\u003cli\u003e路由拦截统一封装思路\u003c/li\u003e\n\u003cli\u003etabBar配置规则，自定义tabBar实现\u003c/li\u003e\n\u003cli\u003e自定义tabBar踩坑点\u003c/li\u003e\n\u003cli\u003e页面跳转后保留表单状态方案\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"七网络请求与后端交互\"\u003e七、网络请求与后端交互\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003ewx.request请求基础配置\u003c/li\u003e\n\u003cli\u003erequest合法域名校验规则，开发环境规避\u003c/li\u003e\n\u003cli\u003e小程序请求携带cookie机制\u003c/li\u003e\n\u003cli\u003e请求header统一封装token思路\u003c/li\u003e\n\u003cli\u003e拦截器封装wx.request方案\u003c/li\u003e\n\u003cli\u003e并发请求、请求超时处理\u003c/li\u003e\n\u003cli\u003e文件上传wx.uploadFile参数配置\u003c/li\u003e\n\u003cli\u003e文件下载wx.downloadFile使用场景\u003c/li\u003e\n\u003cli\u003ewebsocket实时通信完整流程\u003c/li\u003e\n\u003cli\u003esocket断线重连实现逻辑\u003c/li\u003e\n\u003cli\u003e接口返回加密数据解密处理\u003c/li\u003e\n\u003cli\u003e批量请求统一捕获异常\u003c/li\u003e\n\u003cli\u003e小程序不支持跨域的根本原因\u003c/li\u003e\n\u003cli\u003e上传图片压缩、裁剪处理\u003c/li\u003e\n\u003cli\u003e上传进度监听API\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"八登录授权用户信息\"\u003e八、登录、授权、用户信息\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003ewx.login获取code作用\u003c/li\u003e\n\u003cli\u003ecode换取openid完整流程\u003c/li\u003e\n\u003cli\u003eopenid、unionid区别与生成规则\u003c/li\u003e\n\u003cli\u003e隐私保护新规下获取用户头像昵称方案\u003c/li\u003e\n\u003cli\u003egetUserInfo接口废弃替代方案\u003c/li\u003e\n\u003cli\u003e手机号一键登录流程\u003c/li\u003e\n\u003cli\u003e授权scope各权限含义\u003c/li\u003e\n\u003cli\u003e用户拒绝授权后重新引导授权逻辑\u003c/li\u003e\n\u003cli\u003e登录态session_key过期处理\u003c/li\u003e\n\u003cli\u003e第三方登录、关联账号实现\u003c/li\u003e\n\u003cli\u003e登录状态持久化存储方案\u003c/li\u003e\n\u003cli\u003e多端打通unionid获取条件\u003c/li\u003e\n\u003cli\u003e隐私协议弹窗实现规范\u003c/li\u003e\n\u003cli\u003e小程序订阅消息下发流程\u003c/li\u003e\n\u003cli\u003e模板消息与订阅消息区别\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"九媒体图片地图设备api\"\u003e九、媒体、图片、地图、设备API\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003ewx.chooseMedia选择图片视频参数\u003c/li\u003e\n\u003cli\u003e图片预览wx.previewImage使用\u003c/li\u003e\n\u003cli\u003e图片本地缓存方案\u003c/li\u003e\n\u003cli\u003e录音、播放音频API使用\u003c/li\u003e\n\u003cli\u003evideo视频播放常见问题\u003c/li\u003e\n\u003cli\u003emap地图组件标记、覆盖物绘制\u003c/li\u003e\n\u003cli\u003e获取当前定位wx.getLocation权限配置\u003c/li\u003e\n\u003cli\u003e后台持续定位配置要求\u003c/li\u003e\n\u003cli\u003e设备信息、系统API获取\u003c/li\u003e\n\u003cli\u003e振动、剪贴板、屏幕亮度API\u003c/li\u003e\n\u003cli\u003e扫码wx.scanCode场景\u003c/li\u003e\n\u003cli\u003e蓝牙BLE设备连接流程\u003c/li\u003e\n\u003cli\u003eNFC基础使用场景\u003c/li\u003e\n\u003cli\u003e相机camera组件适配\u003c/li\u003e\n\u003cli\u003e文件管理FileSystemManager读写文件\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十性能优化专项\"\u003e十、性能优化专项\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003esetData性能优化手段\u003c/li\u003e\n\u003cli\u003e长列表渲染优化方案\u003c/li\u003e\n\u003cli\u003e虚拟列表实现思路\u003c/li\u003e\n\u003cli\u003e分包加载优化首屏速度\u003c/li\u003e\n\u003cli\u003e图片懒加载处理\u003c/li\u003e\n\u003cli\u003e减少页面、组件层级嵌套\u003c/li\u003e\n\u003cli\u003ewxss样式体积压缩优化\u003c/li\u003e\n\u003cli\u003e分包预加载preloadRule配置\u003c/li\u003e\n\u003cli\u003e避免频繁创建定时器、监听事件\u003c/li\u003e\n\u003cli\u003e页面卸载清除所有订阅、定时器\u003c/li\u003e\n\u003cli\u003e包体积超限优化方案\u003c/li\u003e\n\u003cli\u003e开发者工具性能面板排查卡顿\u003c/li\u003e\n\u003cli\u003e避免在onShow频繁请求接口\u003c/li\u003e\n\u003cli\u003e图片资源压缩、webp适配\u003c/li\u003e\n\u003cli\u003e减少observers深层监听开销\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十一适配兼容多端\"\u003e十一、适配、兼容、多端\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003erpx、px、rem转换关系\u003c/li\u003e\n\u003cli\u003e刘海屏、安全区适配css写法\u003c/li\u003e\n\u003cli\u003e深色模式适配配置\u003c/li\u003e\n\u003cli\u003e不同微信版本API兼容处理\u003c/li\u003e\n\u003cli\u003ecaniuse判断接口是否可用\u003c/li\u003e\n\u003cli\u003e小程序转H5、转uni-app差异\u003c/li\u003e\n\u003cli\u003e平板、PC端小程序适配要点\u003c/li\u003e\n\u003cli\u003e低版本微信兼容降级方案\u003c/li\u003e\n\u003cli\u003e字体、图片多设备适配\u003c/li\u003e\n\u003cli\u003e自定义导航栏适配胶囊按钮\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十二工程化第三方框架\"\u003e十二、工程化、第三方框架\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e原生小程序、uni-app、Taro区别\u003c/li\u003e\n\u003cli\u003eTaro编译原理，多端适配逻辑\u003c/li\u003e\n\u003cli\u003euni-app分包、条件编译写法\u003c/li\u003e\n\u003cli\u003e小程序npm依赖使用规则\u003c/li\u003e\n\u003cli\u003e按需引入UI组件库优化\u003c/li\u003e\n\u003cli\u003e工程化打包、环境变量区分\u003c/li\u003e\n\u003cli\u003e多环境请求域名配置\u003c/li\u003e\n\u003cli\u003eESLint校验小程序代码规范\u003c/li\u003e\n\u003cli\u003eGit协同开发规范\u003c/li\u003e\n\u003cli\u003eCI自动上传小程序工具\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十三支付分享广告业务能力\"\u003e十三、支付、分享、广告、业务能力\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e小程序支付完整流程\u003c/li\u003e\n\u003cli\u003e支付回调后端处理逻辑\u003c/li\u003e\n\u003cli\u003e分享onShareAppMessage、onShareTimeline配置\u003c/li\u003e\n\u003cli\u003e分享图、分享路径动态处理\u003c/li\u003e\n\u003cli\u003e分享到朋友圈限制条件\u003c/li\u003e\n\u003cli\u003e插屏广告、激励视频广告接入\u003c/li\u003e\n\u003cli\u003e小程序跳转其他小程序API\u003c/li\u003e\n\u003cli\u003e跳转限制与关联配置要求\u003c/li\u003e\n\u003cli\u003e客服消息接入流程\u003c/li\u003e\n\u003cli\u003e商品卡片、搜索收录配置sitemap\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十四安全审核合规\"\u003e十四、安全、审核、合规\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e小程序合法域名、业务域名、web-view域名区别\u003c/li\u003e\n\u003cli\u003e前端存储敏感信息安全风险\u003c/li\u003e\n\u003cli\u003esession_key不能传输到前端原因\u003c/li\u003e\n\u003cli\u003e接口数据防篡改简单方案\u003c/li\u003e\n\u003cli\u003e隐私合规要求有哪些\u003c/li\u003e\n\u003cli\u003e图片、内容审核API调用\u003c/li\u003e\n\u003cli\u003e小程序常见审核驳回原因\u003c/li\u003e\n\u003cli\u003e诱导分享、诱导支付违规点\u003c/li\u003e\n\u003cli\u003e本地存储数据加密思路\u003c/li\u003e\n\u003cli\u003e防止恶意刷接口方案\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十五实战场景综合题\"\u003e十五、实战场景综合题\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e项目整体目录架构设计\u003c/li\u003e\n\u003cli\u003e封装通用请求库完整思路\u003c/li\u003e\n\u003cli\u003e全局登录拦截、未登录跳转实现\u003c/li\u003e\n\u003cli\u003e大型表单组件封装与校验\u003c/li\u003e\n\u003cli\u003e多页面共享登录状态管理\u003c/li\u003e\n\u003cli\u003e列表上拉加载、下拉刷新封装\u003c/li\u003e\n\u003cli\u003e图片上传、预览、删除完整流程\u003c/li\u003e\n\u003cli\u003e分包改造解决主包体积超限\u003c/li\u003e\n\u003cli\u003e页面返回保留搜索条件、分页数据\u003c/li\u003e\n\u003cli\u003e小程序热更新主动检测逻辑\u003c/li\u003e\n\u003cli\u003ewebsocket聊天断线重连方案\u003c/li\u003e\n\u003cli\u003e自定义tabBar遇到的坑与解决\u003c/li\u003e\n\u003cli\u003e页面大量数据滑动卡顿优化\u003c/li\u003e\n\u003cli\u003e线上白屏、加载失败排查思路\u003c/li\u003e\n\u003cli\u003e原生小程序迁移Taro/uni改造方案\u003c/li\u003e\n\u003c/ol\u003e","title":"微信小程序 相关面试题整理"},{"content":"为什么我们需要两种冲突解决策略？ 哈希表的核心矛盾在于：哈希函数将无限的定义域映射到有限的地址空间。根据鸽巢原理（Pigeonhole Principle），冲突是必然的。于是，计算机科学家们在\u0026quot;时间换空间\u0026ldquo;和\u0026rdquo;空间换时间\u0026ldquo;的永恒博弈中，诞生了两大流派：\n开放定址法（Open Addressing）：所有元素都存储在数组本身，冲突时寻找其他空位——原地消化。 链地址法（Separate Chaining）：数组存储指针，冲突的元素在数组外形成链表——外部延展。 这两者看似只是实现细节的差异，实则反映了对内存模型、缓存行为、并发控制、最坏情况保证等底层哲学的不同理解。\n一、线性探测（Linear Probing）：极简主义的得与失 1.1 算法精要 线性探测的规则极其简单：\n插入: h(k), h(k)+1, h(k)+2, ... (mod m) 查找: 同插入序列，直到找到或遇到空位 删除: 标记为\u0026#34;墓碑\u0026#34;(DELETED)，不能物理清空 关键洞察：线性探测的查找路径是确定性的，这意味着它拥有完美的CPU缓存预取特性（Prefetching），在现代CPU架构下，连续内存访问的速度比指针跳转快一个数量级。\n1.2 主聚集（Primary Clustering）的数学本质 这是线性探测最致命的缺陷，也是理解其性能天花板的钥匙。\n假设负载因子为 α（α = n/m），当插入一个新元素时，它在某个位置发生冲突的概率不是简单的 α，而是该位置所在的连续占用块的长度占比。这个正反馈机制导致：\n一旦形成长度为 L 的连续占用块，新元素落入该块的概率为 L/m 落入后，块长度变为 L+1，进一步增加未来冲突概率 最终导致占用块像雪崩一样快速增长 数学结论（Knuth 的经典分析）：\n成功查找的平均探测次数 ≈ 0.5 * (1 + 1/(1-α)) 插入的平均探测次数 ≈ 0.5 * (1 + 1/(1-α)^2) 当 α = 0.9 时，插入需要平均约 50.5 次探测，性能崩溃。这也是为什么线性探测要求负载因子严格控制在 0.7 以下。\n1.3 删除与墓碑的隐藏代价 线性探测的删除必须使用墓碑，这带来三重悲剧：\n空间污染：墓碑占用的位置无法被新数据直接覆盖（需要判断是否允许覆盖，复杂化逻辑） 查询退化：大量的墓碑会导致查找遍历过长的已删除序列 需要周期性重建（Rehash）：当墓碑比例超过阈值（如 30%），需要重新插入所有存活元素 // 墓碑导致的查找效率下降示意 [ A ] [ 墓碑 ] [ 墓碑 ] [ 墓碑 ] [ B ] [ 空 ] // 查找 B：需要跳过 3 个墓碑，即使它们是空的，线性探测依然要检查 1.4 线性探测的\u0026quot;真香\u0026quot;场景 尽管有上述缺陷，线性探测在工业界依然被广泛使用，原因在于：\n场景一：纯内存缓存系统 Google 的 flat_hash_map（Abseil 库）、Facebook 的 folly::F14Hash 等现代高性能哈希表，都大量采用混合开放定址法（如 SwissTable 的元数据探测）。它们的核心逻辑是：利用 SIMD 指令一次性检查 16 个槽位，将线性探测的\u0026quot;逐个对比\u0026quot;升级为\u0026quot;批量对比\u0026rdquo;，把主聚集的影响降到极低。\n场景二：读多写少的稳定负载 在负载因子固定且不高（如 α = 0.5）的只读或极少删除场景，线性探测的缓存友好性使其成为绝对王者，性能可以超过链地址法 2-3 倍。\n场景三：实时系统 线性探测没有动态内存分配（malloc），避免了不可预测的延迟抖动，适合金融高频交易、游戏引擎等硬实时场景。\n二、链地址法（Separate Chaining）：鲁棒性的胜利 2.1 基本实现与变种 标准链地址法的核心是：数组每个槽位是一个链表的头指针。\n[0] -\u0026gt; (k1,v1) -\u0026gt; (k4,v4) -\u0026gt; null [1] -\u0026gt; (k2,v2) -\u0026gt; null [2] -\u0026gt; null [3] -\u0026gt; (k3,v3) -\u0026gt; (k5,v5) -\u0026gt; (k6,v6) -\u0026gt; null 2.2 从链表到红黑树：Java HashMap 的进化 Java 8 对 HashMap 的革命性改进，完美诠释了链地址法的演进方向：\n为什么引入红黑树？\n理论分析：当哈希函数质量差或遭遇恶意攻击时，链表长度可能退化为 O(n) 实测数据：链表长度为 8 时，查找耗时大约是红黑树的 3 倍；长度为 64 时，差距超过 20 倍 阈值选择（8 → 树化，6 → 退化）：基于泊松分布统计，负载因子 0.75 下，链表长度达到 8 的概率低于 千万分之一，这是一个防止极端攻击的防御性设计 树化带来的代价：\n红黑树节点占用内存约为链表节点的 2 倍（需要存储 parent、left、right、color） 树化操作本身需要 O(n) 时间 因此只在链表长度 ≥ 8 且 数组容量 ≥ 64 时才触发 2.3 空间复杂度的隐形成本 链地址法看似可以支持任意负载因子（α \u0026gt; 1.0），但代价是每个键值对都需要额外的指针开销：\nJava 8 HashMap 的 Node 对象：约 32 字节（对象头 + key + value + next + hash） 而开放定址法只需要数组槽位本身（通常 8-16 字节） 在存储海量小对象（如百万级整数键值对）时，链地址法的内存开销可能是线性探测的 2-3 倍。\n2.4 并发环境下的天然优势 这是链地址法最容易被忽视的优势：\n细粒度锁：在 ConcurrentHashMap 中，锁的粒度是单个桶（bucket）。两个线程操作不同桶时完全无竞争 线性探测的扩容噩梦：开放定址法扩容时，所有元素都需要重新计算位置并迁移，涉及大范围的数组复制。虽然可以采用渐进式 Rehash（如 Redis），但实现复杂度陡增 三、双雄对决：8 个维度的全面对比 维度 线性探测（开放定址法） 链地址法（HashMap 为代表） 最佳负载因子 ≤ 0.7（超过 0.7 性能急剧下降） ≤ 0.75（可容忍 \u0026gt; 1.0） 查找最坏复杂度 O(n)（且常数较大，因为要扫描连续块） O(log n)（红黑树优化后） 内存布局 连续内存，CPU 缓存命中率极高 指针链，内存碎片化，缓存不友好 删除操作 墓碑标记 + 周期性重建，复杂度高 链表/树节点删除，O(1) 或 O(log n) 动态内存分配 无（预分配数组），延迟稳定 每次插入可能触发 malloc，延迟不可预测 扩容开销 全量迁移，O(n) 且无法避免 渐进式扩容，可分批完成 并发友好性 差（扩容需锁整个表） 优秀（桶级锁 + CAS 无锁化） 空间效率 极高（除墓碑外无额外开销） 较低（每个节点有指针 + 对象头） 四、工业界的妥协与智慧：混合方案 现实世界中，没有\u0026quot;银弹\u0026quot;。现代工程实践往往采用混合策略：\n4.1 Google SwissTable（Abseil flat_hash_map） 核心思想：元数据（metadata）和实际数据分离存储 探测方式：每个槽位有一个 1 字节的元数据（7 bits 用于哈希指纹，1 bit 表示是否占用） 优化点：使用 SSE2/AVX2 指令一次检查 16 个元数据，找到匹配的候选槽后再回查实际数据 结果：将线性探测的查找复杂度从 O(探测次数) 降为 O(1) + 少量 SIMD 指令，同时保持内存连续性 4.2 Redis 字典的渐进式 Rehash 使用链地址法，但扩容时采用两个哈希表并存 每次操作时顺带迁移一部分槽位，将 O(n) 的扩容均摊到 O(1) 牺牲了瞬时性能，换取了可预测的延迟 4.3 缓存友好型链地址法：头插 vs 尾插 头插法（Java 7 之前）：新节点插入链表头部，插入 O(1) 但遍历顺序与插入顺序相反，缓存不友好 尾插法（Java 8+）：新节点插入尾部，遍历时按插入顺序，更符合局部性原理 关键转折：Java 7 的头插法在并发扩容时产生循环链表死循环，是 Java 8 改为尾插法的直接原因 五、面试官最爱的三道追问 Q1：为什么 Java HashMap 的负载因子是 0.75，而不是 0.7 或 0.8？ 这是一个经典的数学权衡：\n泊松分布：在 0.75 负载下，链表长度 ≥ 8 的概率 ≈ 0.00000006（极低） 空间与时间的黄金分割：0.75 是经验值，使得哈希表在空间浪费（25% 空槽）和查询效率之间达到平衡 Q2：线性探测的 \u0026ldquo;墓碑\u0026rdquo; 会导致表满时死循环吗？ 会的。如果不做特殊处理，当表满且存在墓碑时，查找一个不存在的元素可能会无限循环。解决方案：\n维护一个 元素计数器，当 size == capacity 时拒绝插入或强制扩容 查找时设置一个探测次数上限（如 capacity），超过则判定不存在 Q3：在内存极度受限的嵌入式系统中，选哪种？ 开放定址法。原因：\n嵌入式系统的堆（heap）通常极小，链地址法的动态内存分配（malloc）可能随时失败 预分配的连续数组可以精确控制内存上限 没有指针开销，能存储更多键值对 六、未来的方向：NUMA 感知与持久内存 随着硬件演进，哈希表的冲突策略也在改变：\nNUMA（非统一内存访问）架构：线性探测的连续内存访问可能跨 NUMA 节点，导致远程内存访问延迟激增。链地址法的节点分散可能在不同节点上，反而能利用本地内存。 持久内存（PMEM）：链地址法的指针需要维护 PMEM 上的地址，崩溃恢复时重建链表复杂；线性探测只需扫描连续内存，恢复极快。 结论：没有永远正确的选择，只有场景适配的权衡。\n最后的思考 用一个比喻来总结：\n线性探测像一个纪律严明的军队——每个人都必须排在连续的位置上，行动高效（缓存友好），但一旦有人离开（删除），阵型就乱了（墓碑），需要花大力气重整（Rehash）。\n链地址法像一个自由市场——每个摊位（桶）后面可以无限延伸，灵活性极高，但找东西时可能要在长长的巷子里（链表）穿梭，而且每个商户都要自己搭建铺面（Node 对象），成本更高。\n","permalink":"https://lv-blog.pages.dev/posts/programming/backend/%E7%BA%BF%E6%80%A7%E6%8E%A2%E6%B5%8B%E4%B8%8E%E9%93%BE%E5%9C%B0%E5%9D%80%E6%B3%95%E7%9A%84%E6%B7%B1%E5%BA%A6%E5%89%96%E6%9E%90/","summary":"\u003ch2 id=\"为什么我们需要两种冲突解决策略\"\u003e为什么我们需要两种冲突解决策略？\u003c/h2\u003e\n\u003cp\u003e哈希表的核心矛盾在于：\u003cstrong\u003e哈希函数将无限的定义域映射到有限的地址空间\u003c/strong\u003e。根据鸽巢原理（Pigeonhole Principle），冲突是必然的。于是，计算机科学家们在\u0026quot;\u003cstrong\u003e时间换空间\u003c/strong\u003e\u0026ldquo;和\u0026rdquo;\u003cstrong\u003e空间换时间\u003c/strong\u003e\u0026ldquo;的永恒博弈中，诞生了两大流派：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e开放定址法（Open Addressing）\u003c/strong\u003e：所有元素都存储在数组本身，冲突时寻找其他空位——\u003cstrong\u003e原地消化\u003c/strong\u003e。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e链地址法（Separate Chaining）\u003c/strong\u003e：数组存储指针，冲突的元素在数组外形成链表——\u003cstrong\u003e外部延展\u003c/strong\u003e。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e这两者看似只是实现细节的差异，实则反映了对\u003cstrong\u003e内存模型、缓存行为、并发控制、最坏情况保证\u003c/strong\u003e等底层哲学的不同理解。\u003c/p\u003e\n\u003ch2 id=\"一线性探测linear-probing极简主义的得与失\"\u003e一、线性探测（Linear Probing）：极简主义的得与失\u003c/h2\u003e\n\u003ch3 id=\"11-算法精要\"\u003e1.1 算法精要\u003c/h3\u003e\n\u003cp\u003e线性探测的规则极其简单：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e插入: h(k), h(k)+1, h(k)+2, ... (mod m)\n查找: 同插入序列，直到找到或遇到空位\n删除: 标记为\u0026#34;墓碑\u0026#34;(DELETED)，不能物理清空\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003cstrong\u003e关键洞察\u003c/strong\u003e：线性探测的查找路径是\u003cstrong\u003e确定性的\u003c/strong\u003e，这意味着它拥有完美的CPU缓存预取特性（Prefetching），在现代CPU架构下，连续内存访问的速度比指针跳转快一个数量级。\u003c/p\u003e\n\u003ch3 id=\"12-主聚集primary-clustering的数学本质\"\u003e1.2 主聚集（Primary Clustering）的数学本质\u003c/h3\u003e\n\u003cp\u003e这是线性探测最致命的缺陷，也是理解其性能天花板的钥匙。\u003c/p\u003e\n\u003cp\u003e假设负载因子为 α（α = n/m），当插入一个新元素时，它在某个位置发生冲突的概率不是简单的 α，而是\u003cstrong\u003e该位置所在的连续占用块的长度占比\u003c/strong\u003e。这个正反馈机制导致：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e一旦形成长度为 L 的连续占用块，新元素落入该块的概率为 L/m\u003c/li\u003e\n\u003cli\u003e落入后，块长度变为 L+1，进一步增加未来冲突概率\u003c/li\u003e\n\u003cli\u003e最终导致\u003cstrong\u003e占用块像雪崩一样快速增长\u003c/strong\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e数学结论\u003c/strong\u003e（Knuth 的经典分析）：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e成功查找的平均探测次数 ≈ \u003ccode\u003e0.5 * (1 + 1/(1-α))\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e插入的平均探测次数 ≈ \u003ccode\u003e0.5 * (1 + 1/(1-α)^2)\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e当 α = 0.9 时，插入需要平均约 \u003cstrong\u003e50.5 次\u003c/strong\u003e探测，性能崩溃。这也是为什么线性探测要求负载因子严格控制在 \u003cstrong\u003e0.7 以下\u003c/strong\u003e。\u003c/p\u003e\n\u003ch3 id=\"13-删除与墓碑的隐藏代价\"\u003e1.3 删除与墓碑的隐藏代价\u003c/h3\u003e\n\u003cp\u003e线性探测的删除必须使用墓碑，这带来三重悲剧：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e空间污染\u003c/strong\u003e：墓碑占用的位置无法被新数据直接覆盖（需要判断是否允许覆盖，复杂化逻辑）\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e查询退化\u003c/strong\u003e：大量的墓碑会导致查找遍历过长的已删除序列\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e需要周期性重建\u003c/strong\u003e（Rehash）：当墓碑比例超过阈值（如 30%），需要重新插入所有存活元素\u003c/li\u003e\n\u003c/ol\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-cpp\" data-lang=\"cpp\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 墓碑导致的查找效率下降示意\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e[ A ] [ \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e墓碑\u003c/span\u003e ] [ \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e墓碑\u003c/span\u003e ] [ \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e墓碑\u003c/span\u003e ] [ B ] [ \u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e空\u003c/span\u003e ]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 查找 B：需要跳过 3 个墓碑，即使它们是空的，线性探测依然要检查\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"14-线性探测的真香场景\"\u003e1.4 线性探测的\u0026quot;真香\u0026quot;场景\u003c/h3\u003e\n\u003cp\u003e尽管有上述缺陷，线性探测在工业界依然被广泛使用，原因在于：\u003c/p\u003e","title":"线性探测与链地址法的剖析"},{"content":"模板一：网络服务万金油（只要有网络端口，通杀） 如果容器是 Web 服务（如 Nginx、Tomcat、Node.js）、数据库（如 Redis、MySQL）或者各种中间件，只要它暴露了 TCP 端口，且容器内带有最基础的 sh，这就是最无敌的万金油配置。\n它利用了 Linux 内核原生 \u0026lt;/dev/tcp 漏洞，完全不依赖 curl 或 wget 。\nhealthcheck: # 语法解释：尝试与容器本地的 8080 端口建立 TCP 连接，失败则退出码为 1 test: [\u0026#34;CMD-SHELL\u0026#34;, \u0026#34;sh -c \u0026#39;\u0026lt;/dev/tcp/127.0.0.1/8080\u0026#39; || exit 1\u0026#34;] interval: 10s # 10秒探一次，既保证敏锐度，又不会给系统带来TCP连接压力 timeout: 3s # 握手超过3秒没反应，说明网络栈或主线程已经卡死 retries: 3 # 连续失败3次（大约30秒内）正式宣判死刑 start_period: 30s # 30秒新手保护期，给服务腾出启动和绑定端口的时间 注：使用时只需要把 8080 改成你容器内部的实际端口（如 Redis 改成 6379，Nacos 改成 8848）即可。\n模板二：Java/Spring Boot 专属万金油（解决高 CPU 与 Full GC 假死） 对于 Java 应用，有时候端口虽然勉强能连上，但实际上 JVM 内部因为内存溢出（OOM）或者恐怖的 CPU 占用，已经完全无法处理业务了。\n这时候用 Java 自带的诊断工具 jcmd 去拍 JVM 的脑袋，是最精准的万金油手段（前提是使用 JDK 镜像，非精简的 JRE）。\nhealthcheck: # 语法解释：让 jcmd 去问容器内 PID 为 1 的 Java 进程要一个性能计数器数据 # 如果 JVM 彻底卡死或正在发生毁灭性的长时间 Full GC，该命令会直接超时或报错 test: [\u0026#34;CMD-SHELL\u0026#34;, \u0026#34;jcmd 1 PerfCounter.print || exit 1\u0026#34;] interval: 15s # Java 应用体检可以稍微放缓一点频率 timeout: 5s # 给 JVM 5秒的响应时间 retries: 3 start_period: 60s # 【重点】Java 启动普遍较慢（如重型 Spring Cloud 框架），保护期至少给 60 秒起步 模板三：极度精简/无命令容器的“外部看门狗”万金油 如果容器是纯静态的、或者用 Go 写的极简镜像，里面既没有 sh 也没有任何命令，那就采用反客为主的万金油策略——把监控挂在宿主机上。\n直接在宿主机的定时任务（Crontab）里挂这行万金油命令，一行搞定所有同类容器：\n* * * * * docker ps --format \u0026#34;{{.Names}}\u0026#34; | xargs -I {} sh -c \u0026#39;docker inspect --format \u0026#34;{{.State.Health.Status}}\u0026#34; {} | grep -q \u0026#34;unhealthy\u0026#34; \u0026amp;\u0026amp; docker restart {}\u0026#39; 【这行命令在干嘛？】 它每分钟自动扫描你机器上所有正在运行的 Docker 容器。只要发现任何一个容器的健康状态变成了 \u0026quot;unhealthy\u0026quot;，它就会自动出手执行 docker restart 帮它复活。\n有了这行宿主机命令，你只需要在那些有条件的容器里写上 healthcheck，后续的“卡死自动重启”逻辑就再也不用每个容器单独配置看门狗了。\n能测 业务层（HTTP API） 最好； 没有工具就退而求其次测 网络层（TCP 端口）； 连网络都没有就测 系统进程（JVM/PID）。 ","permalink":"https://lv-blog.pages.dev/posts/programming/backend/docker-healthcheck-%E4%B8%87%E9%87%91%E6%B2%B9/","summary":"\u003ch3 id=\"模板一网络服务万金油只要有网络端口通杀\"\u003e模板一：网络服务万金油（只要有网络端口，通杀）\u003c/h3\u003e\n\u003cp\u003e如果容器是 Web 服务（如 Nginx、Tomcat、Node.js）、数据库（如 Redis、MySQL）或者各种中间件，只要它\u003cstrong\u003e暴露了 TCP 端口\u003c/strong\u003e，且容器内带有最基础的 \u003ccode\u003esh\u003c/code\u003e，这就是最无敌的万金油配置。\u003c/p\u003e\n\u003cp\u003e它利用了 Linux 内核原生 \u003ccode\u003e\u0026lt;/dev/tcp\u003c/code\u003e 漏洞，完全不依赖 \u003ccode\u003ecurl\u003c/code\u003e 或 \u003ccode\u003ewget\u003c/code\u003e 。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003ehealthcheck\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#75715e\"\u003e# 语法解释：尝试与容器本地的 8080 端口建立 TCP 连接，失败则退出码为 1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003etest\u003c/span\u003e: [\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;CMD-SHELL\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;sh -c \u0026#39;\u0026lt;/dev/tcp/127.0.0.1/8080\u0026#39; || exit 1\u0026#34;\u003c/span\u003e]\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003einterval\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e10s\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e# 10秒探一次，既保证敏锐度，又不会给系统带来TCP连接压力\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003etimeout\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e3s\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e# 握手超过3秒没反应，说明网络栈或主线程已经卡死\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003eretries\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e3\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e# 连续失败3次（大约30秒内）正式宣判死刑\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003estart_period\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e30s\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e# 30秒新手保护期，给服务腾出启动和绑定端口的时间\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cem\u003e注：使用时只需要把 \u003ccode\u003e8080\u003c/code\u003e 改成你容器内部的实际端口（如 Redis 改成 6379，Nacos 改成 8848）即可。\u003c/em\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"模板二javaspring-boot-专属万金油解决高-cpu-与-full-gc-假死\"\u003e模板二：Java/Spring Boot 专属万金油（解决高 CPU 与 Full GC 假死）\u003c/h3\u003e\n\u003cp\u003e对于 Java 应用，有时候端口虽然勉强能连上，但实际上 JVM 内部因为内存溢出（OOM）或者恐怖的 CPU 占用，已经完全无法处理业务了。\u003c/p\u003e\n\u003cp\u003e这时候用 Java 自带的诊断工具 \u003ccode\u003ejcmd\u003c/code\u003e 去拍 JVM 的脑袋，是最精准的万金油手段（前提是使用 JDK 镜像，非精简的 JRE）。\u003c/p\u003e","title":"Docker Healthcheck 万金油"},{"content":"正在更新中\n","permalink":"https://lv-blog.pages.dev/posts/projects/sh/%E8%BF%87%E5%AE%B6%E5%AE%B6%E9%A1%B9%E7%9B%AE%E6%88%91%E6%98%AF%E5%A6%82%E4%BD%95%E8%AE%BE%E8%AE%A1%E4%BA%B2%E5%B1%9E%E7%A7%B0%E8%B0%93%E8%AE%A1%E7%AE%97%E5%BC%95%E6%93%8E%E7%9A%84/","summary":"\u003cp\u003e正在更新中\u003c/p\u003e","title":"过家家项目：我是如何设计亲属称谓计算引擎的？"},{"content":" 开发者警告：这是一个 Alpha 版本 Github 网址： 部分功能仍在测试阶段，稳定性有待验证。请勿擅自用于任何商业用途，仅供学习参考\n🏡 SweetHome — 过家家 项目官网：https://www.sweethome.asia 面向家庭的社交与智能生活平台后端 从家庭关系图谱出发，重新定义家人之间的连接方式 功能特性 · 系统架构 · 技术栈 · 模块说明 · 快速开始 · API 概览 · 设计亮点 APP页面展示 ✨ 功能特性 👨‍👩‍👧‍👦 家庭关系管理 家庭创建与加入 — 注册即创建家庭，或通过邀请码/管理员审批加入 亲属关系引擎 — 基于图的 BFS 最短路径算法，自动计算成员间的称谓（爷爷、舅舅、堂姐\u0026hellip;），服务端只产出语言无关的 relationCode，本地化由客户端完成 多家庭切换 — 用户同一时刻只属于一个家庭，切换时自动级联退出旧家庭 💬 即时通讯 WebSocket 实时聊天 — 原生 WebSocket（非 STOMP），支持文本/图片/语音/视频/红包消息 Redis Pub/Sub 广播 — 支持多实例水平扩展，消息通过 Redis 频道在各实例间扇出 HTTP 兜底 — WebSocket 断线时降级到 REST 接口发送 未读计数 — 基于游标的消息状态跟踪 💰 电子红包 Lua 脚本保证原子性 — 抢红包操作在 Redis 中原子执行，支持高并发 Redis Stream 持久化 — 红包领取记录先进入 Stream，由后台消费者异步落库 自动过期 — 定时任务将过期红包退款 📍 位置服务 实时位置上报 — 家庭成员位置追踪 电子围栏 — 自定义地理围栏，越界告警（通过极光推送通知） 📸 家庭动态（Moments） 图文/视频动态 — 类似朋友圈的家庭动态 点赞与评论 — 互动功能 公共动态广场 — 跨家庭动态聚合 ❤️ 健康管理 健康指标记录 — 身高、体重、血压，按日追踪 可见性控制 — 每项指标可独立设置对家人公开/私密 每日提醒 — 定时推送提醒记录健康数据 🔐 安全认证 JWT 双 Token — accessToken（15 分钟）+ refreshToken（30 天） 网关统一认证 — 所有请求统一在 Gateway 层鉴权，下游服务信任 X-User-Id 头 RSA 非对称签名 — 仅 auth-service 持有私钥签发，其他服务只持公钥验证 ☁️ 云原生基础设施 Nacos — 服务注册发现 + 配置中心（共享配置） Dubbo — 服务间 RPC 调用（最长 3 跳链路：auth → user → family → chat） Kafka — 事件驱动（缓存失效广播、围栏告警、离线通知、健康提醒） JPush — 极光推送，App 后台时接收告警通知 Caffeine + Redis — 两级缓存，热点数据就近加速 Cloudflare R2 — 对象存储（头像、聊天图片、视频、语音） 🌐多语言支持 ✅ 中文简体 ✅ 中文繁体 ✅ 英语 ✅ 缅甸语（AI翻译，未进行人工测试） ✅ 韩语（AI翻译，未进行人工测试） ✅ 日语（AI翻译，未进行人工测试）\n🏗 系统架构 ┌─────────────────────────────────────────────┐ │ Spring Cloud Gateway :8080 │ │ 统一认证 · 路由转发 · 白名单 · JWT 验证 │ └──────────┬────────────────┬────────────────┘ │ │ ┌────────────┴──────┐ ┌──────┴──────────────┐ │ REST Routes │ │ WebSocket Route │ │ /v1/auth/** │ │ /v1/ws │ │ /v1/users/** │ └──────────┬──────────┘ │ /v1/families/** │ │ │ /v1/conversations│ ┌──────────┴──────────┐ │ /v1/location/** │ │ chat-service │ │ /v1/moment/** │ │ WebSocket Handler │ │ /v1/health/** │ │ Redis Pub/Sub │ │ /v1/redpacket/** │ └─────────────────────┘ └────────┬─────────┘ │ ┌────────────────────┼────────────────────────────┐ │ │ │ ┌────┴─────┐ ┌──────┴──────┐ ┌──────┴──────┐ │ auth-svc │ │ user-svc │ │ family-svc │ │ :8081 │◄────►│ :8082 │◄───────────►│ :8083 │ │ JWT签发 │ Dubbo│ 用户/上传 │ Dubbo │ 家庭/亲属关系 │ └──────────┘ └──────┬──────┘ └──────┬──────┘ │ │ ┌──────┴──────┐ ┌──────┴──────┐ │ location-svc│ │ moment-svc │ │ :8085 │ │ :8086 │ │ 位置/围栏 │ │ 动态/点赞/评论│ └─────────────┘ └─────────────┘ ┌──────────────────┼──────────────────┐ ┌─────┴──────┐ ┌──────┴──────┐ ┌─────┴──────┐ │ health-svc │ │redpacket-svc│ │ chat-svc │ │ :8087 │ │ :8088 │ │ :8084 │ │ 健康记录/提醒│ │ 红包/抢红包 │ │ 会话/消息 │ └────────────┘ └─────────────┘ └────────────┘ 基础设施层 ┌─────────────────────────────────────────┐ │ Docker Compose │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ Nacos │ │ MySQL │ │ Redis │ │ │ │ :8848 │ │ :3306 │ │ :6379 │ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ ┌─────────┐ ┌─────────┐ │ │ │ Kafka │ │ Kafdrop│ │ │ │ :9092 │ │ :9000 │ │ │ └─────────┘ └─────────┘ │ └─────────────────────────────────────────┘ 请求生命周期 客户端请求 │ ▼ ┌──────────────────────────────────────────────────────────┐ │ Gateway (AuthGlobalFilter) │ │ 1. 检查白名单（登录/注册/刷新/家庭预览） │ │ 2. 从 Authorization 头或 ?token= 取 JWT │ │ 3. RSA 公钥验签 + 检查 type=access │ │ 4. 删除外部 X-User-Id，写入真实 userId │ │ 5. 转发到下游微服务 │ └──────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────┐ │ 微服务 (UserContextInterceptor) │ │ 1. 读取 X-User-Id 请求头 │ │ 2. 写入 ThreadLocal (UserContext) │ │ 3. 业务代码调用 UserContext.getUserId() │ │ 4. 请求结束后清除 ThreadLocal │ └──────────────────────────────────────────────────────────┘ │ ▼ ┌──────────────────────────────────────────────────────────┐ │ 返回统一格式 Result\u0026lt;T\u0026gt; │ │ { \u0026#34;code\u0026#34;: 200, \u0026#34;message\u0026#34;: \u0026#34;success\u0026#34;, \u0026#34;data\u0026#34;: {...} } │ └──────────────────────────────────────────────────────────┘ 🛠 技术栈 类别 技术 用途 语言 Java 17 开发和运行 框架 Spring Boot 3.5, Spring Cloud 2025 微服务基础设施 服务治理 Spring Cloud Alibaba 2025, Nacos 2.4 注册发现 \u0026amp; 配置中心 RPC Apache Dubbo 3.3 微服务间远程调用 网关 Spring Cloud Gateway (WebFlux) 统一入口 \u0026amp; 认证 ORM MyBatis-Plus 3.5 数据持久化 缓存 Caffeine (L1) + Redis 7 (L2) 两级缓存加速 消息队列 Apache Kafka 3.9 事件驱动 \u0026amp; 异步解耦 实时通信 WebSocket + Redis Pub/Sub 即时聊天 \u0026amp; 多实例广播 数据库 MySQL 8.0 关系型数据存储 对象存储 Cloudflare R2 (S3 兼容) 文件存储（头像/图片/视频/语音） 推送 极光推送 (JPush) App 离线通知 安全 JWT (RSA-256), BCrypt 认证 \u0026amp; 密码加密 构建 Maven Wrapper 项目构建 \u0026amp; 依赖管理 📦 模块说明 模块 端口 Dubbo 端口 说明 common — — 共享核心库：统一响应体 Result\u0026lt;T\u0026gt;、异常体系 ErrorCode/BusinessException/GlobalExceptionHandler、UserContext（ThreadLocal）、Dubbo 异常过滤器、共享常量 api — — Dubbo 接口契约（UserApi/FamilyApi/ChatApi）+ 共享 DTO/VO，供 provider/consumer 共同依赖 gateway 8080 — 统一网关：路由转发 + AuthGlobalFilter（JWT 统一验证）+ CORS auth-service 8081 20880 认证中心：注册/登录/登出/刷新 Token，RSA JWT 签发，BCrypt 密码 user-service 8082 20881 用户服务：个人信息、头像/图片/视频/语音上传（R2）、推送 Token 注册、两级缓存 family-service 8083 20882 家庭服务：家庭 CRUD、成员管理、邀请码、申请审批、KinshipEngine（亲属称谓计算） chat-service 8084 20883 聊天服务：会话管理、消息历史、WebSocket 实时通信、Redis 多实例广播 location-service 8085 20884 位置服务：实时位置上报、电子围栏、越界告警 moment-service 8086 20885 动态服务：家庭动态（朋友圈）、点赞/评论、公共动态广场 health-service 8087 20886 健康服务：身高/体重/血压纪录、可见性控制、每日提醒推送 redpacket-service 8088 20887 红包服务：发红包/抢红包、Redis Lua 原子操作、异步落库、过期退款 shop-service — — 商城服务（预留模块） ai-service — — AI 服务（预留模块） 🚀 快速开始 前置要求 JDK 17+ Docker \u0026amp; Docker Compose Maven Wrapper（项目自带 ./mvnw） 1. 启动基础设施 docker compose -f dockercompose.yml up -d 将启动：\nNacos :8848 — 服务注册中心 \u0026amp; 配置中心 MySQL 8.0 :3306 — 数据库（自动执行初始化建表脚本） Redis 7 :6379 — 缓存 \u0026amp; WebSocket Pub/Sub Kafka 3.9 :9092 — 事件总线 Kafdrop :9000 — Kafka 可视化 GUI 2. 配置环境变量 cp .env.example .env 编辑 .env，填入必要配置：\nMYSQL_ROOT_PASSWORD=your_password MYSQL_SERVER_ADDR=localhost NACOS_SERVER_ADDR=localhost NACOS_NAMESPACE=dev # 生成 RSA 密钥对: 运行 auth-service 下的 KeyGenerator.java JWT_PRIVATE_KEY=your_base64_private_key JWT_PUBLIC_KEY=your_base64_public_key # 极光推送（可选） JPUSH_APP_KEY=your_app_key JPUSH_MASTER_SECRET=your_master_secret # Cloudflare R2（可选，不上传文件可忽略） R2_ACCESS_KEY_ID=your_r2_key R2_SECRET_ACCESS_KEY=your_r2_secret R2_ENDPOINT=https://your-account.r2.cloudflarestorage.com R2_PUBLIC_BASE_URL=https://pub.your-domain.com 3. 配置 Nacos 在 Nacos 控制台（http://localhost:8848）创建以下共享配置（DEFAULT_GROUP）：\ncommon-spring.yaml — 通用 Spring 配置（数据源、Redis、Kafka） common-logs.yaml — 通用日志配置 common-mybatis.yaml — MyBatis-Plus 配置 详细配置项见各服务的 application.yml。\n4. 构建 \u0026amp; 启动服务 # 构建全部模块 ./mvnw clean package -DskipTests # 按依赖顺序启动服务（每个新开终端） ./mvnw spring-boot:run -pl auth-service ./mvnw spring-boot:run -pl user-service ./mvnw spring-boot:run -pl family-service ./mvnw spring-boot:run -pl chat-service ./mvnw spring-boot:run -pl location-service ./mvnw spring-boot:run -pl moment-service ./mvnw spring-boot:run -pl health-service ./mvnw spring-boot:run -pl redpacket-service 或使用你喜欢的 IDE（IntelliJ IDEA）逐个启动。\n📖 API 概览 完整 API 文档见 doc/API.md（2424 行，涵盖全部接口）。\n认证服务 方法 路径 说明 POST /v1/auth/register 注册（创建或加入家庭） POST /v1/auth/login 登录 POST /v1/auth/refresh 刷新 Token POST /v1/auth/logout 登出 用户服务 | 方法 | 路径 | 说明 | | | - | \u0026ndash; | | GET | /v1/users/me | 当前用户信息 | | PUT | /v1/users/me | 更新个人信息 | | POST | /v1/users/upload/avatar | 上传头像 | | POST | /v1/users/upload/image | 上传聊天图片 | | POST | /v1/users/upload/video | 上传视频 | | POST | /v1/users/upload/audio | 上传语音 | | POST | /v1/users/push-token | 注册推送 Token | | DELETE | /v1/users/push-token | 注销推送 Token |\n家庭服务 方法 路径 说明 GET /v1/families/lookup 凭邀请码预览家庭 GET /v1/families/{id} 家庭详情 GET /v1/families/{id}/members 家庭成员列表（含称谓） POST /v1/families/{id}/invite 生成邀请码 POST /v1/families/join 凭邀请码加入 POST /v1/families/join-requests 提交加入申请 GET /v1/families/{id}/join-requests 查看申请列表 POST /v1/families/join-requests/{id}/approve 批准申请 POST /v1/families/join-requests/{id}/reject 拒绝申请 聊天服务 | 方法 | 路径 | 说明 | | - | | | | GET | /v1/conversations | 会话列表 | | POST | /v1/conversations | 创建私聊 | | GET | /v1/conversations/{id}/messages | 消息历史（游标分页） | | POST | /v1/conversations/{id}/messages | 发送消息（HTTP 兜底） | | PUT | /v1/conversations/{id}/read | 标记已读 | | WS | /v1/ws?token= | WebSocket 实时通信 |\n位置服务 · 动态服务 · 健康服务 · 红包服务 详见 doc/API.md 对应章节。\n💡 设计亮点 🧬 亲属关系引擎 family-service 核心创新。将家庭建模为有向图（成员=节点，血缘/姻亲=边），通过 BFS 搜索最短路径并编码为 F/M/S/Son/Dau tokens，再将绕路路径折叠简化（如 F.Son → 兄弟），最终产出语言无关的 relationCode。本地化翻译完全交由客户端处理，支持任意语言而无需修改后端。\n🚪 网关统一认证 AuthGlobalFilter 作为单一安全关口：白名单放行登录/注册，其余请求统一验签，删除并重写 X-User-Id 头。下游服务零信任外部请求，彻底杜绝用户 ID 伪造。\n💬 水平扩展的 WebSocket 架构 聊天服务通过 Redis Pub/Sub 实现跨实例消息广播。每个实例维护本地 userId → WebSocketSession 映射，收到消息后发布到 conv:\u0026lt;id\u0026gt; 频道，所有实例订阅并推送给本地的接收方连接。\n🔄 原子抢红包 基于 Redis Lua 脚本 实现红包抢夺的原子性，一次网络往返完成余额检查、扣减、记录，避免高并发下的超抢。红包领取流水先写入 Redis Stream 再异步消费落库，保证性能的同时不丢数据。\n🧩 事件驱动架构 基于 Kafka + Outbox 模式 实现可靠事件发布：\n用户缓存失效广播 — user-service L1 缓存更新时通知其他实例 电子围栏报警 — location-service 检测越界 → Kafka → user-service → JPush 聊天离线通知 — 目标用户不在线时，通过 Kafka 触发推送 健康提醒 — 定时调度 → Kafka → JPush 📐 统一响应 \u0026amp; 异常体系 全局 Result\u0026lt;T\u0026gt; 包装 + ErrorCode 枚举 + BusinessException + GlobalExceptionHandler，确保所有接口返回结构一致的 JSON 错误信息。Dubbo 层通过 DubboExceptionFilter 跨服务传播业务异常，不丢失错误码。\n📁 项目结构 sh/ ├── common/ # 共享核心库 │ └── src/main/java/asia/sweethome/common/ │ ├── constants/ # 共享常量（ConversationType, MessageType, RelationType...） │ ├── context/ # UserContext (ThreadLocal) │ ├── dubbo/ # DubboExceptionFilter │ ├── entity/vo/ # Result\u0026lt;T\u0026gt; 统一响应体 │ └── exception/ # ErrorCode, BusinessException, GlobalExceptionHandler │ ├── api/ # Dubbo 接口契约 │ └── src/main/java/asia/sweethome/api/ │ ├── entity/dto/ # 共享请求/响应 DTO │ ├── UserApi.java # 用户 Dubbo 接口 │ ├── FamilyApi.java # 家庭 Dubbo 接口 │ └── ChatApi.java # 聊天 Dubbo 接口 │ ├── gateway/ # 服务网关 :8080 ├── auth-service/ # 认证中心 :8081 ├── user-service/ # 用户服务 :8082 ├── family-service/ # 家庭服务 :8083 ├── chat-service/ # 聊天服务 :8084 ├── location-service/ # 位置服务 :8085 ├── moment-service/ # 动态服务 :8086 ├── health-service/ # 健康服务 :8087 ├── redpacket-service/ # 红包服务 :8088 ├── shop-service/ # 商城（预留） ├── ai-service/ # AI 服务（预留） │ ├── doc/ # 文档 │ ├── API.md # 完整 API 文档 │ ├── PORTS.md # 端口分配表 │ ├── ROADMAP.md # 开发路线图 │ └── ... │ ├── docker/ # Docker 配置 │ └── mysql/mysql-init/ # 数据库初始化 SQL │ ├── dockercompose.yml # 基础设施编排 ├── pom.xml # 父 POM ├── .env # 本地环境变量 └── CLAUDE.md # Claude Code 开发指南 🧪 关于测试 当前项目以教学/展示为目的，测试覆盖有限。auth-service 提供了一个 KeyGenerator.java（独立 main 方法），用于生成 RSA JWT 密钥对。\n🤝 贡献指南 Fork 本仓库 创建特性分支 (git checkout -b feature/amazing-feature) 提交变更 (git commit -m 'feat: 添加某个很棒的功能') 推送到分支 (git push origin feature/amazing-feature) 创建 Pull Request 代码风格：\n遵循项目现有的代码风格和架构约定 保持中文教学注释的写作风格 统一使用 Result\u0026lt;T\u0026gt; 作为响应包装 业务异常使用 ErrorCode 枚举 + BusinessException 🗺️ ROADMAP 正在开发 手机短信验证 OAuth 支持更多登录方式 还在后头 家庭AI助手\n天然适配APP，使用便捷 获取一手家庭成员详细信息 获取家庭成员实时位置 获取家庭成员最近发布的动态（了解心理状况等） 收集一切可获取的事件信息，生成每日总结报告 指导家庭积极向上，促进家庭和谐发展 邻里互惠(家庭小卖部系统)\n内部销售：家庭成员间闲置物品互通有无，其他成员可请求获取 外部销售：其他家庭可浏览并选择请求获取或直接下单购买 基于熟人信任机制，交易过程透明，避免扯皮纠纷 家庭详细信息\n记录出生日期（作为AI参考依据，可自动计算年龄） 家庭排行榜\n步数统计：前端统计，后端存储数据 关心统计：分析成员间消息互动频率，识别最关心彼此的成员组合 对互动不足的家庭成员自动提醒，鼓励多多关注家人 家庭相册\n动态相册：实时更新的家庭照片集 专用相册：按主题或事件分类整理 家庭影音档案：统一的音视频资料库 老人照料功能\n手机跌倒检测，异常时及时提醒 适配大按钮，方便老人使用 家庭OA系统（财务审批）\n孩子零花钱征用申请流程 （更多OA功能待扩展） 家庭自律系统\n24小时监测手机使用情况 自动生成使用报告并上报 家庭倒数日/大事件\n记录家庭成员生日倒计时\n重要纪念日提醒\n族谱功能增强\n支持族谱成员新增\n每个成员对应唯一邀请码\n新成员凭邀请码注册并入家\n📄 许可证 本项目仅供个人学习和交流使用。\nSweetHome · 过家家 家的温暖，一触即达 ","permalink":"https://lv-blog.pages.dev/posts/projects/sh/%E8%BF%87%E5%AE%B6%E5%AE%B6%E9%A1%B9%E7%9B%AE%E4%B8%80%E6%AC%BE%E4%B8%93%E6%B3%A8%E4%BA%8E%E5%AE%B6%E5%BA%AD%E7%9A%84%E5%BA%94%E7%94%A8/","summary":"\u003cblockquote\u003e\n\u003ch3 id=\"开发者警告这是一个-alpha-版本\"\u003e开发者警告：这是一个 Alpha 版本\u003c/h3\u003e\n\u003cp\u003eGithub 网址：\n部分功能仍在测试阶段，稳定性有待验证。请勿擅自用于任何商业用途，仅供学习参考\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cdiv style=\"display: flex; flex-wrap: wrap; gap: 10px 10px;justify-content:center\"\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Java-17-ED8B00?logo=java\u0026logoColor=white\" alt=\"Java 17\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Spring_Boot-3.5-6DB33F?logo=springboot\u0026logoColor=white\" alt=\"Spring Boot 3.5\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Spring_Cloud-2025-6DB33F?logo=spring\u0026logoColor=white\" alt=\"Spring Cloud 2025\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Apache_Dubbo-3.3-FB542B?logo=apachedubbo\u0026logoColor=white\" alt=\"Dubbo 3.3\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Nacos-2.4-1890FF?logo=alibabacloud\u0026logoColor=white\" alt=\"Nacos 2.4\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/MyBatis_Plus-3.5-DA392E?logo=mybatis\u0026logoColor=white\" alt=\"MyBatis-Plus\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Redis-7-FF4438?logo=redis\u0026logoColor=white\" alt=\"Redis 7\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/Apache_Kafka-3.9-231F20?logo=apachekafka\u0026logoColor=white\" alt=\"Kafka 3.9\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/MySQL-8.0-4479A1?logo=mysql\u0026logoColor=white\" alt=\"MySQL 8.0\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/WebSocket-实时-FF6B35?logo=socket.io\u0026logoColor=white\" alt=\"WebSocket\"/\u003e\n  \u003cimg src=\"https://img.shields.io/badge/JPush-极光推送-4A90D9\" alt=\"JPush\"/\u003e\n\u003c/div\u003e\n\u003ch1 align=\"center\"\u003e🏡 SweetHome — 过家家\u003c/h1\u003e\n\u003ch3 align=\"center\"\u003e项目官网：\u003ca href=\"https://www.sweethome.asia\"\u003ehttps://www.sweethome.asia\u003c/a\u003e\u003c/h3\u003e\n\u003cp align=\"center\"\u003e\n  \u003cb\u003e面向家庭的社交与智能生活平台后端\u003c/b\u003e\u003cbr/\u003e\n  \u003ci\u003e从家庭关系图谱出发，重新定义家人之间的连接方式\u003c/i\u003e\n\u003c/p\u003e\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"#-功能特性\"\u003e功能特性\u003c/a\u003e ·\n  \u003ca href=\"#-系统架构\"\u003e系统架构\u003c/a\u003e ·\n  \u003ca href=\"#-技术栈\"\u003e技术栈\u003c/a\u003e ·\n  \u003ca href=\"#-模块说明\"\u003e模块说明\u003c/a\u003e ·\n  \u003ca href=\"#-快速开始\"\u003e快速开始\u003c/a\u003e ·\n  \u003ca href=\"#-API-概览\"\u003eAPI 概览\u003c/a\u003e ·\n  \u003ca href=\"#-设计亮点\"\u003e设计亮点\u003c/a\u003e\n\u003c/p\u003e\n\u003ccenter\u003e\nAPP页面展示\u003c/center\u003e\n\u003cdiv align=\"center\" style=\"display:flex\"\u003e\n  \u003cimg src=\"https://img.wathan.cn/images/2026/07/b207f431a4ed0a849ab5e571aaaee2380220540c1810cb4081c8732959d80c6d.webp\" width=\"30%\" /\u003e\n  \u003cimg src=\"https://img.wathan.cn/images/2026/07/ee822b9db77a733c3f871e3448a83b5b8d7ed83f3aff8bdc8b46710f1bdd2d09.webp\" width=\"30%\" /\u003e\n  \u003cimg src=\"https://img.wathan.cn/images/2026/07/ec6635a636390abe4fd3cc8f9205585e4ea63edf45323b50878c59bb7aa9c716.webp\" width=\"30%\" /\u003e\n\u003c/div\u003e\n\u003ch2 id=\"-功能特性\"\u003e✨ 功能特性\u003c/h2\u003e\n\u003ch3 id=\"-家庭关系管理\"\u003e👨‍👩‍👧‍👦 家庭关系管理\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e家庭创建与加入\u003c/strong\u003e — 注册即创建家庭，或通过邀请码/管理员审批加入\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e亲属关系引擎\u003c/strong\u003e — 基于图的 BFS 最短路径算法，自动计算成员间的称谓（爷爷、舅舅、堂姐\u0026hellip;），\u003cstrong\u003e服务端只产出语言无关的 \u003ccode\u003erelationCode\u003c/code\u003e，本地化由客户端完成\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e多家庭切换\u003c/strong\u003e — 用户同一时刻只属于一个家庭，切换时自动级联退出旧家庭\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"-即时通讯\"\u003e💬 即时通讯\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eWebSocket 实时聊天\u003c/strong\u003e — 原生 WebSocket（非 STOMP），支持文本/图片/语音/视频/红包消息\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRedis Pub/Sub 广播\u003c/strong\u003e — 支持多实例水平扩展，消息通过 Redis 频道在各实例间扇出\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eHTTP 兜底\u003c/strong\u003e — WebSocket 断线时降级到 REST 接口发送\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e未读计数\u003c/strong\u003e — 基于游标的消息状态跟踪\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"-电子红包\"\u003e💰 电子红包\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eLua 脚本保证原子性\u003c/strong\u003e — 抢红包操作在 Redis 中原子执行，支持高并发\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRedis Stream 持久化\u003c/strong\u003e — 红包领取记录先进入 Stream，由后台消费者异步落库\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e自动过期\u003c/strong\u003e — 定时任务将过期红包退款\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"-位置服务\"\u003e📍 位置服务\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e实时位置上报\u003c/strong\u003e — 家庭成员位置追踪\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e电子围栏\u003c/strong\u003e — 自定义地理围栏，越界告警（通过极光推送通知）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"-家庭动态moments\"\u003e📸 家庭动态（Moments）\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e图文/视频动态\u003c/strong\u003e — 类似朋友圈的家庭动态\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e点赞与评论\u003c/strong\u003e — 互动功能\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e公共动态广场\u003c/strong\u003e — 跨家庭动态聚合\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"-健康管理\"\u003e❤️ 健康管理\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e健康指标记录\u003c/strong\u003e — 身高、体重、血压，按日追踪\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e可见性控制\u003c/strong\u003e — 每项指标可独立设置对家人公开/私密\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e每日提醒\u003c/strong\u003e — 定时推送提醒记录健康数据\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"-安全认证\"\u003e🔐 安全认证\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eJWT 双 Token\u003c/strong\u003e — accessToken（15 分钟）+ refreshToken（30 天）\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e网关统一认证\u003c/strong\u003e — 所有请求统一在 Gateway 层鉴权，下游服务信任 \u003ccode\u003eX-User-Id\u003c/code\u003e 头\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRSA 非对称签名\u003c/strong\u003e — 仅 auth-service 持有私钥签发，其他服务只持公钥验证\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"-云原生基础设施\"\u003e☁️ 云原生基础设施\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eNacos\u003c/strong\u003e — 服务注册发现 + 配置中心（共享配置）\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eDubbo\u003c/strong\u003e — 服务间 RPC 调用（最长 3 跳链路：auth → user → family → chat）\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eKafka\u003c/strong\u003e — 事件驱动（缓存失效广播、围栏告警、离线通知、健康提醒）\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eJPush\u003c/strong\u003e — 极光推送，App 后台时接收告警通知\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCaffeine + Redis\u003c/strong\u003e — 两级缓存，热点数据就近加速\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCloudflare R2\u003c/strong\u003e — 对象存储（头像、聊天图片、视频、语音）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"多语言支持\"\u003e🌐多语言支持\u003c/h3\u003e\n\u003cp\u003e✅ 中文简体\n✅ 中文繁体\n✅ 英语\n✅ 缅甸语（AI翻译，未进行人工测试）\n✅ 韩语（AI翻译，未进行人工测试）\n✅ 日语（AI翻译，未进行人工测试）\u003c/p\u003e","title":"过家家项目：一款专注于家庭的应用"},{"content":"一、基础类型与字面量（1-10） int 最大值 2,147,483,647（2³¹-1），最小值 -2,147,483,648（-2³¹） long 最大值 9,223,372,036,854,775,807（2⁶³-1），字面量加 L float 字面量必须加 f，否则默认 double short 范围 -32,768 ~ 32,767，byte 范围 -128 ~ 127 字面量下划线：1_000_000 等于 1000000，仅 Java 7+ 0x 开头是十六进制，0b 开头是二进制，0 开头是八进制（容易误判） char 占 2 字节，Character.SIZE = 16，但 Unicode 扩展字符需 2 个 char（surrogate pair） boolean 只有 true/false，不能和 0/1 互转（和 C 不同） Integer.valueOf(127) == Integer.valueOf(127) 为 true，128 为 false（缓存池 -128~127） Integer.parseInt(\u0026quot;\u0026quot;) 抛 NumberFormatException，Integer.parseInt(null) 也抛 二、运算符与表达式（11-18） \u0026amp; 和 \u0026amp;\u0026amp; 区别：\u0026amp; 两边都执行（位运算或逻辑与），\u0026amp;\u0026amp; 短路 | 和 || 同理，|| 短路 ^ 按位异或，~ 按位取反（包含符号位） \u0026gt;\u0026gt; 算术右移（补符号位），\u0026gt;\u0026gt;\u0026gt; 无符号右移（补 0） i++ 和 ++i 的字节码区别：前者先加载后自增，后者先自增后加载 += 隐式类型转换：short s = 1; s += 1; 合法，s = s + 1; 编译错误 三元运算符 ? : 必须返回相同类型，否则自动类型提升 字符串拼接用 + 在循环内会生成大量 StringBuilder，循环外编译器自动优化 三、String 相关（19-28） String 不可变，底层 char[] 被 final 修饰且不暴露修改方法 字符串常量池在堆中（Java 8+），intern() 手动入池 new String(\u0026quot;abc\u0026quot;) 创建 2 个对象（常量池 + 堆），\u0026quot;abc\u0026quot; 只创建 1 个（若池中没有） String.equals() 比较内容，== 比较引用地址 String.compareTo() 按字典序比较，返回差值 String.format() 使用 %s %d %f 占位符 substring() 在 Java 7 前共享 char[]，Java 7+ 创建新数组（解决内存泄漏） split() 参数是正则，. 需转义为 \\\\. replace() 替换所有字符，replaceAll() 支持正则 StringBuilder 线程不安全，StringBuffer 线程安全（方法加 synchronized），但慢 四、equals 与 hashCode（29-34） 重写 equals 必须重写 hashCode，否则 HashMap/HashSet 失效 equals 必须满足：自反、对称、传递、一致、非空（x.equals(null) 返回 false） hashCode 相等的两个对象 equals 不一定相等（哈希冲突） Objects.equals(a, b) 自动判空，避免 null.equals() Arrays.deepEquals() 比较多维数组，Arrays.equals() 只比较一维 List.equals() 要求顺序相同，Set.equals() 仅要求元素相同（顺序无关） 五、异常处理（35-44） try-catch-finally 中 finally 在 return 之前执行，但返回值已在 finally 前计算好 finally 中有 return 会覆盖 try/catch 的返回值 finally 中修改返回的引用对象内容会生效（如 List.add()） 不要在 finally 中写 return，也不要在其中抛出异常（会掩盖原始异常） try-with-resources 要求资源实现 AutoCloseable，关闭顺序：先声明的后关闭 受检异常（Exception 子类）必须 try-catch 或 throws，RuntimeException 非受检 异常链：catch (SQLException e) { throw new RuntimeException(e); } 保留堆栈 printStackTrace() 不应用在生产日志中，用 log.error(\u0026quot;msg\u0026quot;, e) Throwable 包括 Error 和 Exception，Error 不应该捕获（如 OutOfMemoryError） finally 中关闭资源时可能抛异常，要用 try-catch 包裹或使用 try-with-resources 六、集合框架（45-62） ArrayList 初始容量 10，扩容 1.5 倍（oldCapacity + (oldCapacity \u0026gt;\u0026gt; 1)） ArrayList 随机访问 O(1)，中间插入删除 O(n)，LinkedList 相反 LinkedList 实现了 Deque，可作为队列/栈使用 HashMap 容量为 2 的幂，默认 16，负载因子 0.75，扩容 2 倍 HashMap 索引计算：(n - 1) \u0026amp; hash，hash = key.hashCode() ^ (h \u0026gt;\u0026gt;\u0026gt; 16) Java 8+ HashMap 链表长度 \u0026gt; 8 且容量 \u0026gt;= 64 时转红黑树，\u0026lt; 6 时退化为链表 HashMap 的 key 为 null 时，hash 值为 0，存在 table[0] ConcurrentHashMap 分段锁（Java 7）→ synchronized + CAS（Java 8+） Hashtable 线程安全但全表锁，已淘汰 TreeMap 基于红黑树，有序（按 key 自然序或 Comparator） LinkedHashMap 按插入顺序或访问顺序（accessOrder=true 可实现 LRU） HashSet 底层是 HashMap，TreeSet 底层是 TreeMap PriorityQueue 小顶堆，Comparator.reverseOrder() 可转大顶堆 ArrayDeque 优于 Stack（Stack 已废弃，ArrayDeque 线程不安全但更快） Collections.synchronizedList() 包装为线程安全，但迭代时仍需手动同步 CopyOnWriteArrayList 读多写少场景，写时复制整个数组 List.of() / Set.of()（Java 9+）返回不可变集合，不能增删改 ArrayList.subList() 返回视图，对子列表操作会影响原列表，反之亦然 七、并发与线程（63-80） 创建线程的三种方式：Thread、Runnable、Callable（有返回值） start() 启动线程，run() 只是普通方法调用 synchronized 可锁实例方法、静态方法（锁 Class）、代码块 wait() 释放锁，sleep() 不释放锁，yield() 让出 CPU 但不释放锁 wait()/notify() 必须在 synchronized 块中调用，否则抛 IllegalMonitorStateException notify() 随机唤醒一个，notifyAll() 唤醒全部 死锁四条件：互斥、请求与保持、不可抢占、循环等待 破坏死锁：按固定顺序加锁、tryLock() 超时、使用 ReentrantLock volatile 保证可见性和有序性（禁止指令重排），不保证原子性 AtomicInteger 用 CAS 实现原子操作，incrementAndGet() 底层是 Unsafe.compareAndSwapInt ThreadLocal 用完后必须 remove()，否则线程池复用导致内存泄漏 ThreadLocal 底层是 ThreadLocalMap，key 是弱引用，value 是强引用 ExecutorService 用 shutdown() 优雅关闭，shutdownNow() 立即关闭 线程池核心参数：核心线程数、最大线程数、存活时间、阻塞队列、拒绝策略 四种拒绝策略：AbortPolicy（抛异常）、CallerRunsPolicy（调用者执行）、DiscardPolicy（丢弃）、DiscardOldestPolicy（丢弃最旧） CachedThreadPool 最大线程数 Integer.MAX_VALUE，容易 OOM，慎用 FixedThreadPool 使用无界队列，任务积压可能导致 OOM Future.get() 阻塞，CompletableFuture 异步回调更灵活 八、I/O 与 NIO（81-88） File 只是路径抽象，不表示真实文件，exists() 判断是否存在 FileInputStream 读取字节，FileReader 读取字符（默认编码可能乱码） BufferedReader 用 readLine() 按行读取，Files.readAllLines() 更简洁 InputStreamReader 可指定字符集：new InputStreamReader(new FileInputStream(\u0026quot;a.txt\u0026quot;), StandardCharsets.UTF_8) try-with-resources 自动关闭多个资源，用分号分隔 Path/Paths/Files（Java 7+）替代 File，Files.walk() 遍历目录树 NIO 核心：Channel + Buffer + Selector（多路复用） ByteBuffer 读写切换需调用 flip()，clear() 清空，compact() 压缩 九、JVM 与内存（89-95） JVM 内存区域：堆、栈、方法区（元空间）、程序计数器、本地方法栈 堆分代：年轻代（Eden + S0 + S1）→ 老年代，默认比例 8:1:1 OutOfMemoryError 常见原因：堆 OOM、元空间 OOM、直接内存 OOM、栈溢出 栈溢出 StackOverflowError，递归过深或无终止条件 System.gc() 仅建议 GC，不保证执行 finalize() 已废弃（Java 9+），对象自救不靠谱 类加载器：Bootstrap（rt.jar）→ Extension → Application，双亲委派模型 十、日期与时间（96-100） java.util.Date 可变、线程不安全，已废弃，用 java.time.*（Java 8+） LocalDate / LocalTime / LocalDateTime 不含时区，ZonedDateTime 含时区 Instant 时间戳（秒/纳秒），Duration 计算时间差，Period 计算日期差 格式化用 DateTimeFormatter 线程安全，替代 SimpleDateFormat（线程不安全） LocalDateTime.now() 获取当前时间，parse(\u0026quot;2026-07-09\u0026quot;) 按 ISO 标准解析 十一、语法糖与常见坑（101-110） 泛型编译期擦除，运行时无泛型信息（List\u0026lt;String\u0026gt; 和 List\u0026lt;Integer\u0026gt; 在运行时相同） 可变参数本质是数组，public void method(String... args) 可传数组或逗号分隔 枚举 enum 默认继承 Enum，不能被继承，构造器私有 匿名内部类持有外部类引用（this$0），可能导致内存泄漏 接口默认方法 default，静态方法 static，接口中变量默认 public static final 抽象类可有构造器，接口无构造器 switch 支持 byte/short/char/int/String/enum，不支持 long/float/double break 跳出循环，continue 跳过本次，return 结束方法 标签 label: 可跳出外层循环，但不推荐使用 数组协变：String[] 是 Object[] 的子类，但集合泛型不变 十二、网络与反射（111-118） URL 和 URI 区别：URI 是标识符，URL 是定位符（包含协议） HttpURLConnection 原生 HTTP 客户端，但 Apache HttpClient / OkHttp 更常用 Socket 阻塞 I/O，ServerSocket 监听端口，accept() 阻塞等待连接 反射 Class.forName() 初始化类，ClassLoader.loadClass() 不初始化 Method.invoke() 性能差，但反射能绕过私有访问（setAccessible(true)） Proxy.newProxyInstance() 动态代理只能代理接口，CGLIB 可代理类 注解保留策略：SOURCE（源码）、CLASS（字节码）、RUNTIME（运行时） 序列化实现 Serializable 接口，serialVersionUID 用于版本兼容，不声明则自动生成 十三、设计原则与模式（119-123） 单例双重检查需 volatile，防止指令重排导致半初始化对象被读取 工厂模式解耦，策略模式消除 if-else，观察者模式事件驱动 依赖倒置：依赖抽象而非具体实现 开闭原则：对扩展开放，对修改封闭 里氏替换：子类可以替换父类出现的地方 十四、Linux 与常用命令（124-128） nohup java -jar app.jar \u0026amp; 后台运行，输出到 nohup.out jps 查看 Java 进程，jstack 打印线程堆栈，jmap 查看堆内存，jstat 监控 GC kill -15 优雅关闭（SIGTERM），kill -9 强制杀死（SIGKILL） tail -f app.log 实时查看日志，grep -C 10 \u0026quot;error\u0026quot; app.log 显示上下文 netstat -tlnp 查看端口占用，lsof -i:8080 查看指定端口 十五、数据库与 JDBC（129-133） PreparedStatement 防 SQL 注入，占位符 ?，编译一次执行多次 Statement 拼接字符串有注入风险，不用 ResultSet 游标从 1 开始，next() 移动指针，getString(1) 按索引取 事务隔离级别：读未提交、读已提交、可重复读、串行化 连接池用 HikariCP（默认）、Druid，不要手动创建连接 十六、Maven / Gradle（134-137） Maven 生命周期：clean → compile → test → package → install → deploy provided 作用域表示运行时由容器提供（如 servlet-api） 依赖传递：compile 传递，test 不传递，provided 不传递 版本冲突用 dependencyManagement 锁定版本，或用 exclude 排除 十七、工具与调试（138-142） System.out.println() 性能差，生产用日志框架（SLF4J + Logback） 日志级别：ERROR \u0026gt; WARN \u0026gt; INFO \u0026gt; DEBUG \u0026gt; TRACE assert 默认禁用，需 -ea 开启，生产慎用 Objects.requireNonNull() 判空抛 NullPointerException，比手动 if 简洁 Optional 用来表示可能为 null 的返回值，不要用作字段或参数 十八、编码规范与常识（143-150） 包名全小写，类名大驼峰，方法/变量小驼峰，常量全大写 + 下划线 方法名用动词（getUser），变量名用名词（userName） 布尔变量用 is / has / can 开头（isDeleted） 一个 .java 文件只能有一个 public 类，文件名必须和 public 类名一致 构造器不能 static，不能被 final 修饰 static 块在类加载时执行一次，{} 实例块在构造器前执行 匿名对象（new User()）用完即抛，适合单次使用 永远不要在线上环境用 System.out 打印大对象，更不要用 e.printStackTrace() ","permalink":"https://lv-blog.pages.dev/posts/programming/java%E7%A8%8B%E5%BA%8F%E5%91%98%E9%98%B2%E7%BF%BB%E8%BD%A6%E6%8C%87%E5%8D%97/","summary":"\u003ch2 id=\"一基础类型与字面量1-10\"\u003e一、基础类型与字面量（1-10）\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\u003ccode\u003eint\u003c/code\u003e 最大值 \u003ccode\u003e2,147,483,647\u003c/code\u003e（2³¹-1），最小值 \u003ccode\u003e-2,147,483,648\u003c/code\u003e（-2³¹）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003elong\u003c/code\u003e 最大值 \u003ccode\u003e9,223,372,036,854,775,807\u003c/code\u003e（2⁶³-1），字面量加 \u003ccode\u003eL\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003efloat\u003c/code\u003e 字面量必须加 \u003ccode\u003ef\u003c/code\u003e，否则默认 \u003ccode\u003edouble\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eshort\u003c/code\u003e 范围 \u003ccode\u003e-32,768 ~ 32,767\u003c/code\u003e，\u003ccode\u003ebyte\u003c/code\u003e 范围 \u003ccode\u003e-128 ~ 127\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e字面量下划线：\u003ccode\u003e1_000_000\u003c/code\u003e 等于 \u003ccode\u003e1000000\u003c/code\u003e，仅 Java 7+\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e0x\u003c/code\u003e 开头是十六进制，\u003ccode\u003e0b\u003c/code\u003e 开头是二进制，\u003ccode\u003e0\u003c/code\u003e 开头是八进制（容易误判）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003echar\u003c/code\u003e 占 2 字节，\u003ccode\u003eCharacter.SIZE\u003c/code\u003e = 16，但 Unicode 扩展字符需 2 个 char（surrogate pair）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eboolean\u003c/code\u003e 只有 \u003ccode\u003etrue/false\u003c/code\u003e，不能和 0/1 互转（和 C 不同）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eInteger.valueOf(127) == Integer.valueOf(127)\u003c/code\u003e 为 \u003ccode\u003etrue\u003c/code\u003e，\u003ccode\u003e128\u003c/code\u003e 为 \u003ccode\u003efalse\u003c/code\u003e（缓存池 -128~127）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eInteger.parseInt(\u0026quot;\u0026quot;)\u003c/code\u003e 抛 \u003ccode\u003eNumberFormatException\u003c/code\u003e，\u003ccode\u003eInteger.parseInt(null)\u003c/code\u003e 也抛\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"二运算符与表达式11-18\"\u003e二、运算符与表达式（11-18）\u003c/h2\u003e\n\u003col start=\"11\"\u003e\n\u003cli\u003e\u003ccode\u003e\u0026amp;\u003c/code\u003e 和 \u003ccode\u003e\u0026amp;\u0026amp;\u003c/code\u003e 区别：\u003ccode\u003e\u0026amp;\u003c/code\u003e 两边都执行（位运算或逻辑与），\u003ccode\u003e\u0026amp;\u0026amp;\u003c/code\u003e 短路\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e|\u003c/code\u003e 和 \u003ccode\u003e||\u003c/code\u003e 同理，\u003ccode\u003e||\u003c/code\u003e 短路\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e^\u003c/code\u003e 按位异或，\u003ccode\u003e~\u003c/code\u003e 按位取反（包含符号位）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e\u0026gt;\u0026gt;\u003c/code\u003e 算术右移（补符号位），\u003ccode\u003e\u0026gt;\u0026gt;\u0026gt;\u003c/code\u003e 无符号右移（补 0）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ei++\u003c/code\u003e 和 \u003ccode\u003e++i\u003c/code\u003e 的字节码区别：前者先加载后自增，后者先自增后加载\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e+=\u003c/code\u003e 隐式类型转换：\u003ccode\u003eshort s = 1; s += 1;\u003c/code\u003e 合法，\u003ccode\u003es = s + 1;\u003c/code\u003e 编译错误\u003c/li\u003e\n\u003cli\u003e三元运算符 \u003ccode\u003e? :\u003c/code\u003e 必须返回相同类型，否则自动类型提升\u003c/li\u003e\n\u003cli\u003e字符串拼接用 \u003ccode\u003e+\u003c/code\u003e 在循环内会生成大量 \u003ccode\u003eStringBuilder\u003c/code\u003e，循环外编译器自动优化\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"三string-相关19-28\"\u003e三、String 相关（19-28）\u003c/h2\u003e\n\u003col start=\"19\"\u003e\n\u003cli\u003e\u003ccode\u003eString\u003c/code\u003e 不可变，底层 \u003ccode\u003echar[]\u003c/code\u003e 被 \u003ccode\u003efinal\u003c/code\u003e 修饰且不暴露修改方法\u003c/li\u003e\n\u003cli\u003e字符串常量池在堆中（Java 8+），\u003ccode\u003eintern()\u003c/code\u003e 手动入池\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003enew String(\u0026quot;abc\u0026quot;)\u003c/code\u003e 创建 2 个对象（常量池 + 堆），\u003ccode\u003e\u0026quot;abc\u0026quot;\u003c/code\u003e 只创建 1 个（若池中没有）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eString.equals()\u003c/code\u003e 比较内容，\u003ccode\u003e==\u003c/code\u003e 比较引用地址\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eString.compareTo()\u003c/code\u003e 按字典序比较，返回差值\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eString.format()\u003c/code\u003e 使用 \u003ccode\u003e%s\u003c/code\u003e \u003ccode\u003e%d\u003c/code\u003e \u003ccode\u003e%f\u003c/code\u003e 占位符\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003esubstring()\u003c/code\u003e 在 Java 7 前共享 \u003ccode\u003echar[]\u003c/code\u003e，Java 7+ 创建新数组（解决内存泄漏）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003esplit()\u003c/code\u003e 参数是正则，\u003ccode\u003e.\u003c/code\u003e 需转义为 \u003ccode\u003e\\\\.\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ereplace()\u003c/code\u003e 替换所有字符，\u003ccode\u003ereplaceAll()\u003c/code\u003e 支持正则\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eStringBuilder\u003c/code\u003e 线程不安全，\u003ccode\u003eStringBuffer\u003c/code\u003e 线程安全（方法加 \u003ccode\u003esynchronized\u003c/code\u003e），但慢\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"四equals-与-hashcode29-34\"\u003e四、equals 与 hashCode（29-34）\u003c/h2\u003e\n\u003col start=\"29\"\u003e\n\u003cli\u003e重写 \u003ccode\u003eequals\u003c/code\u003e 必须重写 \u003ccode\u003ehashCode\u003c/code\u003e，否则 \u003ccode\u003eHashMap\u003c/code\u003e/\u003ccode\u003eHashSet\u003c/code\u003e 失效\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eequals\u003c/code\u003e 必须满足：自反、对称、传递、一致、非空（\u003ccode\u003ex.equals(null)\u003c/code\u003e 返回 \u003ccode\u003efalse\u003c/code\u003e）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ehashCode\u003c/code\u003e 相等的两个对象 \u003ccode\u003eequals\u003c/code\u003e 不一定相等（哈希冲突）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eObjects.equals(a, b)\u003c/code\u003e 自动判空，避免 \u003ccode\u003enull.equals()\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eArrays.deepEquals()\u003c/code\u003e 比较多维数组，\u003ccode\u003eArrays.equals()\u003c/code\u003e 只比较一维\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eList.equals()\u003c/code\u003e 要求顺序相同，\u003ccode\u003eSet.equals()\u003c/code\u003e 仅要求元素相同（顺序无关）\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"五异常处理35-44\"\u003e五、异常处理（35-44）\u003c/h2\u003e\n\u003col start=\"35\"\u003e\n\u003cli\u003e\u003ccode\u003etry-catch-finally\u003c/code\u003e 中 \u003ccode\u003efinally\u003c/code\u003e 在 \u003ccode\u003ereturn\u003c/code\u003e 之前执行，但返回值已在 \u003ccode\u003efinally\u003c/code\u003e 前计算好\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003efinally\u003c/code\u003e 中有 \u003ccode\u003ereturn\u003c/code\u003e 会覆盖 \u003ccode\u003etry\u003c/code\u003e/\u003ccode\u003ecatch\u003c/code\u003e 的返回值\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003efinally\u003c/code\u003e 中修改返回的引用对象内容会生效（如 \u003ccode\u003eList.add()\u003c/code\u003e）\u003c/li\u003e\n\u003cli\u003e不要在 \u003ccode\u003efinally\u003c/code\u003e 中写 \u003ccode\u003ereturn\u003c/code\u003e，也不要在其中抛出异常（会掩盖原始异常）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003etry-with-resources\u003c/code\u003e 要求资源实现 \u003ccode\u003eAutoCloseable\u003c/code\u003e，关闭顺序：先声明的后关闭\u003c/li\u003e\n\u003cli\u003e受检异常（\u003ccode\u003eException\u003c/code\u003e 子类）必须 \u003ccode\u003etry-catch\u003c/code\u003e 或 \u003ccode\u003ethrows\u003c/code\u003e，\u003ccode\u003eRuntimeException\u003c/code\u003e 非受检\u003c/li\u003e\n\u003cli\u003e异常链：\u003ccode\u003ecatch (SQLException e) { throw new RuntimeException(e); }\u003c/code\u003e 保留堆栈\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eprintStackTrace()\u003c/code\u003e 不应用在生产日志中，用 \u003ccode\u003elog.error(\u0026quot;msg\u0026quot;, e)\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eThrowable\u003c/code\u003e 包括 \u003ccode\u003eError\u003c/code\u003e 和 \u003ccode\u003eException\u003c/code\u003e，\u003ccode\u003eError\u003c/code\u003e 不应该捕获（如 \u003ccode\u003eOutOfMemoryError\u003c/code\u003e）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003efinally\u003c/code\u003e 中关闭资源时可能抛异常，要用 \u003ccode\u003etry-catch\u003c/code\u003e 包裹或使用 \u003ccode\u003etry-with-resources\u003c/code\u003e\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"六集合框架45-62\"\u003e六、集合框架（45-62）\u003c/h2\u003e\n\u003col start=\"45\"\u003e\n\u003cli\u003e\u003ccode\u003eArrayList\u003c/code\u003e 初始容量 10，扩容 1.5 倍（\u003ccode\u003eoldCapacity + (oldCapacity \u0026gt;\u0026gt; 1)\u003c/code\u003e）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eArrayList\u003c/code\u003e 随机访问 O(1)，中间插入删除 O(n)，\u003ccode\u003eLinkedList\u003c/code\u003e 相反\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eLinkedList\u003c/code\u003e 实现了 \u003ccode\u003eDeque\u003c/code\u003e，可作为队列/栈使用\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eHashMap\u003c/code\u003e 容量为 2 的幂，默认 16，负载因子 0.75，扩容 2 倍\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eHashMap\u003c/code\u003e 索引计算：\u003ccode\u003e(n - 1) \u0026amp; hash\u003c/code\u003e，\u003ccode\u003ehash = key.hashCode() ^ (h \u0026gt;\u0026gt;\u0026gt; 16)\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003eJava 8+ \u003ccode\u003eHashMap\u003c/code\u003e 链表长度 \u0026gt; 8 且容量 \u0026gt;= 64 时转红黑树，\u0026lt; 6 时退化为链表\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eHashMap\u003c/code\u003e 的 \u003ccode\u003ekey\u003c/code\u003e 为 \u003ccode\u003enull\u003c/code\u003e 时，hash 值为 0，存在 \u003ccode\u003etable[0]\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eConcurrentHashMap\u003c/code\u003e 分段锁（Java 7）→ \u003ccode\u003esynchronized\u003c/code\u003e + CAS（Java 8+）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eHashtable\u003c/code\u003e 线程安全但全表锁，已淘汰\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eTreeMap\u003c/code\u003e 基于红黑树，有序（按 key 自然序或 Comparator）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eLinkedHashMap\u003c/code\u003e 按插入顺序或访问顺序（\u003ccode\u003eaccessOrder=true\u003c/code\u003e 可实现 LRU）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eHashSet\u003c/code\u003e 底层是 \u003ccode\u003eHashMap\u003c/code\u003e，\u003ccode\u003eTreeSet\u003c/code\u003e 底层是 \u003ccode\u003eTreeMap\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ePriorityQueue\u003c/code\u003e 小顶堆，\u003ccode\u003eComparator.reverseOrder()\u003c/code\u003e 可转大顶堆\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eArrayDeque\u003c/code\u003e 优于 \u003ccode\u003eStack\u003c/code\u003e（\u003ccode\u003eStack\u003c/code\u003e 已废弃，\u003ccode\u003eArrayDeque\u003c/code\u003e 线程不安全但更快）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eCollections.synchronizedList()\u003c/code\u003e 包装为线程安全，但迭代时仍需手动同步\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eCopyOnWriteArrayList\u003c/code\u003e 读多写少场景，写时复制整个数组\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eList.of()\u003c/code\u003e / \u003ccode\u003eSet.of()\u003c/code\u003e（Java 9+）返回不可变集合，不能增删改\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eArrayList.subList()\u003c/code\u003e 返回视图，对子列表操作会影响原列表，反之亦然\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"七并发与线程63-80\"\u003e七、并发与线程（63-80）\u003c/h2\u003e\n\u003col start=\"63\"\u003e\n\u003cli\u003e创建线程的三种方式：\u003ccode\u003eThread\u003c/code\u003e、\u003ccode\u003eRunnable\u003c/code\u003e、\u003ccode\u003eCallable\u003c/code\u003e（有返回值）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003estart()\u003c/code\u003e 启动线程，\u003ccode\u003erun()\u003c/code\u003e 只是普通方法调用\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003esynchronized\u003c/code\u003e 可锁实例方法、静态方法（锁 Class）、代码块\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ewait()\u003c/code\u003e 释放锁，\u003ccode\u003esleep()\u003c/code\u003e 不释放锁，\u003ccode\u003eyield()\u003c/code\u003e 让出 CPU 但不释放锁\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ewait()\u003c/code\u003e/\u003ccode\u003enotify()\u003c/code\u003e 必须在 \u003ccode\u003esynchronized\u003c/code\u003e 块中调用，否则抛 \u003ccode\u003eIllegalMonitorStateException\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003enotify()\u003c/code\u003e 随机唤醒一个，\u003ccode\u003enotifyAll()\u003c/code\u003e 唤醒全部\u003c/li\u003e\n\u003cli\u003e死锁四条件：互斥、请求与保持、不可抢占、循环等待\u003c/li\u003e\n\u003cli\u003e破坏死锁：按固定顺序加锁、\u003ccode\u003etryLock()\u003c/code\u003e 超时、使用 \u003ccode\u003eReentrantLock\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003evolatile\u003c/code\u003e 保证可见性和有序性（禁止指令重排），不保证原子性\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eAtomicInteger\u003c/code\u003e 用 CAS 实现原子操作，\u003ccode\u003eincrementAndGet()\u003c/code\u003e 底层是 \u003ccode\u003eUnsafe.compareAndSwapInt\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eThreadLocal\u003c/code\u003e 用完后必须 \u003ccode\u003eremove()\u003c/code\u003e，否则线程池复用导致内存泄漏\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eThreadLocal\u003c/code\u003e 底层是 \u003ccode\u003eThreadLocalMap\u003c/code\u003e，key 是弱引用，value 是强引用\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eExecutorService\u003c/code\u003e 用 \u003ccode\u003eshutdown()\u003c/code\u003e 优雅关闭，\u003ccode\u003eshutdownNow()\u003c/code\u003e 立即关闭\u003c/li\u003e\n\u003cli\u003e线程池核心参数：核心线程数、最大线程数、存活时间、阻塞队列、拒绝策略\u003c/li\u003e\n\u003cli\u003e四种拒绝策略：\u003ccode\u003eAbortPolicy\u003c/code\u003e（抛异常）、\u003ccode\u003eCallerRunsPolicy\u003c/code\u003e（调用者执行）、\u003ccode\u003eDiscardPolicy\u003c/code\u003e（丢弃）、\u003ccode\u003eDiscardOldestPolicy\u003c/code\u003e（丢弃最旧）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eCachedThreadPool\u003c/code\u003e 最大线程数 \u003ccode\u003eInteger.MAX_VALUE\u003c/code\u003e，容易 OOM，慎用\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eFixedThreadPool\u003c/code\u003e 使用无界队列，任务积压可能导致 OOM\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eFuture.get()\u003c/code\u003e 阻塞，\u003ccode\u003eCompletableFuture\u003c/code\u003e 异步回调更灵活\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"八io-与-nio81-88\"\u003e八、I/O 与 NIO（81-88）\u003c/h2\u003e\n\u003col start=\"81\"\u003e\n\u003cli\u003e\u003ccode\u003eFile\u003c/code\u003e 只是路径抽象，不表示真实文件，\u003ccode\u003eexists()\u003c/code\u003e 判断是否存在\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eFileInputStream\u003c/code\u003e 读取字节，\u003ccode\u003eFileReader\u003c/code\u003e 读取字符（默认编码可能乱码）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eBufferedReader\u003c/code\u003e 用 \u003ccode\u003ereadLine()\u003c/code\u003e 按行读取，\u003ccode\u003eFiles.readAllLines()\u003c/code\u003e 更简洁\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eInputStreamReader\u003c/code\u003e 可指定字符集：\u003ccode\u003enew InputStreamReader(new FileInputStream(\u0026quot;a.txt\u0026quot;), StandardCharsets.UTF_8)\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003etry-with-resources\u003c/code\u003e 自动关闭多个资源，用分号分隔\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ePath\u003c/code\u003e/\u003ccode\u003ePaths\u003c/code\u003e/\u003ccode\u003eFiles\u003c/code\u003e（Java 7+）替代 \u003ccode\u003eFile\u003c/code\u003e，\u003ccode\u003eFiles.walk()\u003c/code\u003e 遍历目录树\u003c/li\u003e\n\u003cli\u003eNIO 核心：\u003ccode\u003eChannel\u003c/code\u003e + \u003ccode\u003eBuffer\u003c/code\u003e + \u003ccode\u003eSelector\u003c/code\u003e（多路复用）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eByteBuffer\u003c/code\u003e 读写切换需调用 \u003ccode\u003eflip()\u003c/code\u003e，\u003ccode\u003eclear()\u003c/code\u003e 清空，\u003ccode\u003ecompact()\u003c/code\u003e 压缩\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"九jvm-与内存89-95\"\u003e九、JVM 与内存（89-95）\u003c/h2\u003e\n\u003col start=\"89\"\u003e\n\u003cli\u003eJVM 内存区域：堆、栈、方法区（元空间）、程序计数器、本地方法栈\u003c/li\u003e\n\u003cli\u003e堆分代：年轻代（Eden + S0 + S1）→ 老年代，默认比例 8:1:1\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eOutOfMemoryError\u003c/code\u003e 常见原因：堆 OOM、元空间 OOM、直接内存 OOM、栈溢出\u003c/li\u003e\n\u003cli\u003e栈溢出 \u003ccode\u003eStackOverflowError\u003c/code\u003e，递归过深或无终止条件\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eSystem.gc()\u003c/code\u003e 仅建议 GC，不保证执行\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003efinalize()\u003c/code\u003e 已废弃（Java 9+），对象自救不靠谱\u003c/li\u003e\n\u003cli\u003e类加载器：Bootstrap（rt.jar）→ Extension → Application，双亲委派模型\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十日期与时间96-100\"\u003e十、日期与时间（96-100）\u003c/h2\u003e\n\u003col start=\"96\"\u003e\n\u003cli\u003e\u003ccode\u003ejava.util.Date\u003c/code\u003e 可变、线程不安全，已废弃，用 \u003ccode\u003ejava.time.*\u003c/code\u003e（Java 8+）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eLocalDate\u003c/code\u003e / \u003ccode\u003eLocalTime\u003c/code\u003e / \u003ccode\u003eLocalDateTime\u003c/code\u003e 不含时区，\u003ccode\u003eZonedDateTime\u003c/code\u003e 含时区\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eInstant\u003c/code\u003e 时间戳（秒/纳秒），\u003ccode\u003eDuration\u003c/code\u003e 计算时间差，\u003ccode\u003ePeriod\u003c/code\u003e 计算日期差\u003c/li\u003e\n\u003cli\u003e格式化用 \u003ccode\u003eDateTimeFormatter\u003c/code\u003e 线程安全，替代 \u003ccode\u003eSimpleDateFormat\u003c/code\u003e（线程不安全）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eLocalDateTime.now()\u003c/code\u003e 获取当前时间，\u003ccode\u003eparse(\u0026quot;2026-07-09\u0026quot;)\u003c/code\u003e 按 ISO 标准解析\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十一语法糖与常见坑101-110\"\u003e十一、语法糖与常见坑（101-110）\u003c/h2\u003e\n\u003col start=\"101\"\u003e\n\u003cli\u003e泛型编译期擦除，运行时无泛型信息（\u003ccode\u003eList\u0026lt;String\u0026gt;\u003c/code\u003e 和 \u003ccode\u003eList\u0026lt;Integer\u0026gt;\u003c/code\u003e 在运行时相同）\u003c/li\u003e\n\u003cli\u003e可变参数本质是数组，\u003ccode\u003epublic void method(String... args)\u003c/code\u003e 可传数组或逗号分隔\u003c/li\u003e\n\u003cli\u003e枚举 \u003ccode\u003eenum\u003c/code\u003e 默认继承 \u003ccode\u003eEnum\u003c/code\u003e，不能被继承，构造器私有\u003c/li\u003e\n\u003cli\u003e匿名内部类持有外部类引用（\u003ccode\u003ethis$0\u003c/code\u003e），可能导致内存泄漏\u003c/li\u003e\n\u003cli\u003e接口默认方法 \u003ccode\u003edefault\u003c/code\u003e，静态方法 \u003ccode\u003estatic\u003c/code\u003e，接口中变量默认 \u003ccode\u003epublic static final\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e抽象类可有构造器，接口无构造器\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eswitch\u003c/code\u003e 支持 \u003ccode\u003ebyte\u003c/code\u003e/\u003ccode\u003eshort\u003c/code\u003e/\u003ccode\u003echar\u003c/code\u003e/\u003ccode\u003eint\u003c/code\u003e/\u003ccode\u003eString\u003c/code\u003e/\u003ccode\u003eenum\u003c/code\u003e，不支持 \u003ccode\u003elong\u003c/code\u003e/\u003ccode\u003efloat\u003c/code\u003e/\u003ccode\u003edouble\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ebreak\u003c/code\u003e 跳出循环，\u003ccode\u003econtinue\u003c/code\u003e 跳过本次，\u003ccode\u003ereturn\u003c/code\u003e 结束方法\u003c/li\u003e\n\u003cli\u003e标签 \u003ccode\u003elabel:\u003c/code\u003e 可跳出外层循环，但不推荐使用\u003c/li\u003e\n\u003cli\u003e数组协变：\u003ccode\u003eString[]\u003c/code\u003e 是 \u003ccode\u003eObject[]\u003c/code\u003e 的子类，但集合泛型不变\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十二网络与反射111-118\"\u003e十二、网络与反射（111-118）\u003c/h2\u003e\n\u003col start=\"111\"\u003e\n\u003cli\u003e\u003ccode\u003eURL\u003c/code\u003e 和 \u003ccode\u003eURI\u003c/code\u003e 区别：URI 是标识符，URL 是定位符（包含协议）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eHttpURLConnection\u003c/code\u003e 原生 HTTP 客户端，但 Apache HttpClient / OkHttp 更常用\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eSocket\u003c/code\u003e 阻塞 I/O，\u003ccode\u003eServerSocket\u003c/code\u003e 监听端口，\u003ccode\u003eaccept()\u003c/code\u003e 阻塞等待连接\u003c/li\u003e\n\u003cli\u003e反射 \u003ccode\u003eClass.forName()\u003c/code\u003e 初始化类，\u003ccode\u003eClassLoader.loadClass()\u003c/code\u003e 不初始化\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eMethod.invoke()\u003c/code\u003e 性能差，但反射能绕过私有访问（\u003ccode\u003esetAccessible(true)\u003c/code\u003e）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eProxy.newProxyInstance()\u003c/code\u003e 动态代理只能代理接口，CGLIB 可代理类\u003c/li\u003e\n\u003cli\u003e注解保留策略：\u003ccode\u003eSOURCE\u003c/code\u003e（源码）、\u003ccode\u003eCLASS\u003c/code\u003e（字节码）、\u003ccode\u003eRUNTIME\u003c/code\u003e（运行时）\u003c/li\u003e\n\u003cli\u003e序列化实现 \u003ccode\u003eSerializable\u003c/code\u003e 接口，\u003ccode\u003eserialVersionUID\u003c/code\u003e 用于版本兼容，不声明则自动生成\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十三设计原则与模式119-123\"\u003e十三、设计原则与模式（119-123）\u003c/h2\u003e\n\u003col start=\"119\"\u003e\n\u003cli\u003e单例双重检查需 \u003ccode\u003evolatile\u003c/code\u003e，防止指令重排导致半初始化对象被读取\u003c/li\u003e\n\u003cli\u003e工厂模式解耦，策略模式消除 if-else，观察者模式事件驱动\u003c/li\u003e\n\u003cli\u003e依赖倒置：依赖抽象而非具体实现\u003c/li\u003e\n\u003cli\u003e开闭原则：对扩展开放，对修改封闭\u003c/li\u003e\n\u003cli\u003e里氏替换：子类可以替换父类出现的地方\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十四linux-与常用命令124-128\"\u003e十四、Linux 与常用命令（124-128）\u003c/h2\u003e\n\u003col start=\"124\"\u003e\n\u003cli\u003e\u003ccode\u003enohup java -jar app.jar \u0026amp;\u003c/code\u003e 后台运行，输出到 \u003ccode\u003enohup.out\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ejps\u003c/code\u003e 查看 Java 进程，\u003ccode\u003ejstack\u003c/code\u003e 打印线程堆栈，\u003ccode\u003ejmap\u003c/code\u003e 查看堆内存，\u003ccode\u003ejstat\u003c/code\u003e 监控 GC\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ekill -15\u003c/code\u003e 优雅关闭（SIGTERM），\u003ccode\u003ekill -9\u003c/code\u003e 强制杀死（SIGKILL）\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003etail -f app.log\u003c/code\u003e 实时查看日志，\u003ccode\u003egrep -C 10 \u0026quot;error\u0026quot; app.log\u003c/code\u003e 显示上下文\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003enetstat -tlnp\u003c/code\u003e 查看端口占用，\u003ccode\u003elsof -i:8080\u003c/code\u003e 查看指定端口\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十五数据库与-jdbc129-133\"\u003e十五、数据库与 JDBC（129-133）\u003c/h2\u003e\n\u003col start=\"129\"\u003e\n\u003cli\u003e\u003ccode\u003ePreparedStatement\u003c/code\u003e 防 SQL 注入，占位符 \u003ccode\u003e?\u003c/code\u003e，编译一次执行多次\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eStatement\u003c/code\u003e 拼接字符串有注入风险，不用\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eResultSet\u003c/code\u003e 游标从 1 开始，\u003ccode\u003enext()\u003c/code\u003e 移动指针，\u003ccode\u003egetString(1)\u003c/code\u003e 按索引取\u003c/li\u003e\n\u003cli\u003e事务隔离级别：读未提交、读已提交、可重复读、串行化\u003c/li\u003e\n\u003cli\u003e连接池用 HikariCP（默认）、Druid，不要手动创建连接\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十六maven--gradle134-137\"\u003e十六、Maven / Gradle（134-137）\u003c/h2\u003e\n\u003col start=\"134\"\u003e\n\u003cli\u003eMaven 生命周期：\u003ccode\u003eclean\u003c/code\u003e → \u003ccode\u003ecompile\u003c/code\u003e → \u003ccode\u003etest\u003c/code\u003e → \u003ccode\u003epackage\u003c/code\u003e → \u003ccode\u003einstall\u003c/code\u003e → \u003ccode\u003edeploy\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eprovided\u003c/code\u003e 作用域表示运行时由容器提供（如 servlet-api）\u003c/li\u003e\n\u003cli\u003e依赖传递：\u003ccode\u003ecompile\u003c/code\u003e 传递，\u003ccode\u003etest\u003c/code\u003e 不传递，\u003ccode\u003eprovided\u003c/code\u003e 不传递\u003c/li\u003e\n\u003cli\u003e版本冲突用 \u003ccode\u003edependencyManagement\u003c/code\u003e 锁定版本，或用 \u003ccode\u003eexclude\u003c/code\u003e 排除\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十七工具与调试138-142\"\u003e十七、工具与调试（138-142）\u003c/h2\u003e\n\u003col start=\"138\"\u003e\n\u003cli\u003e\u003ccode\u003eSystem.out.println()\u003c/code\u003e 性能差，生产用日志框架（SLF4J + Logback）\u003c/li\u003e\n\u003cli\u003e日志级别：ERROR \u0026gt; WARN \u0026gt; INFO \u0026gt; DEBUG \u0026gt; TRACE\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eassert\u003c/code\u003e 默认禁用，需 \u003ccode\u003e-ea\u003c/code\u003e 开启，生产慎用\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eObjects.requireNonNull()\u003c/code\u003e 判空抛 \u003ccode\u003eNullPointerException\u003c/code\u003e，比手动 \u003ccode\u003eif\u003c/code\u003e 简洁\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003eOptional\u003c/code\u003e 用来表示可能为 null 的返回值，不要用作字段或参数\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"十八编码规范与常识143-150\"\u003e十八、编码规范与常识（143-150）\u003c/h2\u003e\n\u003col start=\"143\"\u003e\n\u003cli\u003e包名全小写，类名大驼峰，方法/变量小驼峰，常量全大写 + 下划线\u003c/li\u003e\n\u003cli\u003e方法名用动词（\u003ccode\u003egetUser\u003c/code\u003e），变量名用名词（\u003ccode\u003euserName\u003c/code\u003e）\u003c/li\u003e\n\u003cli\u003e布尔变量用 \u003ccode\u003eis\u003c/code\u003e / \u003ccode\u003ehas\u003c/code\u003e / \u003ccode\u003ecan\u003c/code\u003e 开头（\u003ccode\u003eisDeleted\u003c/code\u003e）\u003c/li\u003e\n\u003cli\u003e一个 \u003ccode\u003e.java\u003c/code\u003e 文件只能有一个 \u003ccode\u003epublic\u003c/code\u003e 类，文件名必须和 \u003ccode\u003epublic\u003c/code\u003e 类名一致\u003c/li\u003e\n\u003cli\u003e构造器不能 \u003ccode\u003estatic\u003c/code\u003e，不能被 \u003ccode\u003efinal\u003c/code\u003e 修饰\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003estatic\u003c/code\u003e 块在类加载时执行一次，\u003ccode\u003e{}\u003c/code\u003e 实例块在构造器前执行\u003c/li\u003e\n\u003cli\u003e匿名对象（\u003ccode\u003enew User()\u003c/code\u003e）用完即抛，适合单次使用\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e永远不要在线上环境用 \u003ccode\u003eSystem.out\u003c/code\u003e 打印大对象，更不要用 \u003ccode\u003ee.printStackTrace()\u003c/code\u003e\u003c/strong\u003e\u003c/li\u003e\n\u003c/ol\u003e","title":"JAVA程序员防翻车指南"},{"content":"flowchart TD A[2018–2021512–2K Token金鱼记忆时代] --\u003e|只能处理短文、短句、单轮问答| B[2022–20234K–128K Token中上下文普及时代] B --\u003e|可处理论文、合同、小说、中型代码库RAG 成为企业标准方案| C[2024–2025200K–1M Token百万上下文实验期] C --\u003e|纸面支持 1M，但遗忘、成本、速度问题明显仅限量 Beta| D[2026–20271M–2M Token1M 量产普惠时代] D --\u003e|1M 成为旗舰标配可处理整卷宗、整代码库、全年业务日志| E[2028–20292M–10M Token超百万窗口分化时代] E --\u003e|2M 以上面向行业定制长视频、大型代码库、多年日志联合分析| F[2030+10M+ 弹性记忆原生长记忆智能体时代] 一、大模型上下文完整进化时间线（2018–2026，分4个时代） 1. 初代Transformer时代：512–2K（2018–2021，金鱼记忆） 2018 GPT-1 / BERT：512 token，仅短文、短句分类，多轮对话必丢前文 2019 GPT-2：1024 token，能写短篇故事，连续对话3–5轮就遗忘历史 2020 GPT-3：2048 token（2K），行业通用标准，仅支持短提示词、少量示例，长文档必须分段/摘要处理 2021 早期国产（文心一言初代、GLM-1）：统一2K上限，无长文本能力 时代特征：没有原生长文本能力，所有长内容必须靠人工拆分、外部摘要，RAG雏形出现。\n2. 中上下文普及时代：4K–128K（2022–2023，工业化可用） 2022 ChatGPT（GPT-3.5）：4K；年末升级16K，日常聊天够用，长文档仍吃力 2023.3 GPT-4 首发：8K，专业文档、代码单文件处理门槛打开 2023.7 Claude 2：100K，首个量产十万级窗口，法律、长篇论文场景爆发 2023.11 GPT-4 Turbo：128K，全球主流商用标配门槛，单本小说完整载入 2023年底 国产跟进：通义、GLM、混元开放32K/64K；开源Llama 2、Qwen 7B固定4K–32K 2023.12 Gemini 1.0 Ultra 纸面宣称1M，但仅实验室封闭测试，无法商用落地 时代里程碑：128K成为专业AI标配；RAG成为行业标准方案，弥补窗口不足。\n3. 百万上下文实验期：200K–2M纸面（2024–2025，纸面强、实际弱） 2024.2 Gemini 1.5 Pro：原生2M token，全球首个公开百万级模型，但存在严重「中间遗忘Lost in the Middle」，后半段文档召回暴跌，仅适合摘要，不适合精细推理，仅限开发者限量Beta 2024 Claude 3 Opus：200K，稳定可靠，法律行业主力，无严重遗忘问题 2025 上半年：各大厂商放出1M Beta（Claude Sonnet 4、Gemini 2.0），长上下文计费溢价极高、响应慢、显存开销巨大，企业少量试用，普通用户无法接触 2025 国产开源：Qwen、DeepSeek推出64K–128K底座，少量实验版支持256K 时代特征：1M只是技术噱头，标称≠有效；硬件成本、遗忘问题、价格三重门槛，无法大规模落地。\n4. 百万上下文量产普惠时代：1M成为旗舰标配（2026至今，工业稳定可用） 2026.3.13 Claude 4.6 Opus/Sonnet：1M全量GA，取消长文本溢价，长距离推理优化到位，法律/代码全行业落地，标志1M正式商用成熟 2026.4 DeepSeek V4、通义千问3 Max、Qwen3系列：国产闭源/开源全线标配1M，稀疏注意力大幅降低显存成本，单机70B模型可稳定跑1M 2026.6 GLM-5.2、小米MiMo V2-Pro：国产端云双1M，本地端侧下放100K，云端完整1M 2026.7.9 GPT-5.6 Sol 旗舰：150万token，高端分层标配，超长多轮Agent原生支持 现状（2026.7）： 旗舰闭源（GPT/Claude/Gemini）：默认1M起 新一代开源70B+：出厂即1M 中端平价模型逐步下放256K–512K，1M不再是高端专属卖点 时代核心变化：解决长距离遗忘、KV缓存分页、稀疏注意力三大痛点；标称1M = 有效可用1M，RAG不再是刚需，整库、整卷宗一次性读取推理成为常规操作。\n二、各档位上下文窗口：容量、对应内容、核心业务意义 先统一换算标准（中文）： 1K ≈ 750汉字；1M ≈ 75万汉字 ≈ 1500页标准文档\n1）小窗口：2K–16K（日常轻交互，低成本基础模型） 容量对应 2K：1500字短文、10轮简短对话 16K：1.2万字、单篇论文、几十轮聊天记录、单个小型代码文件 适用场景 普通客服聊天、简单文案生成、单轮问答、短摘要、轻量本地部署（手机/笔记本离线模型）\n行业意义 AI普及基础门槛；成本最低、推理最快；无法处理长文档，超过篇幅必须截断或拆分。\n2）中窗口：32K–128K（2023–2025主流专业标准，现在中端标配） 容量对应 32K：2.4万字，几十页合同、中型技术文档 128K：9.6万字，完整长篇小说、300页PDF、5000行代码工程、全年会议纪要单批次载入 适用场景 企业常规文档问答、单项目代码审查、长篇报告总结、多轮长期对话、中小型知识库处理\n行业意义 替代人工分段阅读，单轮完成单份完整资料分析； 是过去两年RAG方案的黄金配套窗口，大部分企业业务在此区间就能满足； 至今仍是平价商用模型主力，性价比最高。 3）大窗口：200K–500K（专业垂直刚需，过渡档位） 容量对应 15–37万字，多份关联合同、全套项目招投标文件、上万行多模块代码库、半年完整客服工单\n适用场景 律所批量案卷、大型项目工程文档、多文件交叉对比、长周期历史对话复盘\n行业意义 介于128K与1M之间；能一次性处理多份关联文档，但仍无法承载完整企业全量资料；2024–2025主流高端模型上限，2026沦为中端过渡规格。\n4）超大窗口：1M（2026旗舰新标准，范式级改变） 容量对应 75万字、1500页资料、整套丛书、完整中型代码仓库、全年完整业务日志、全套诉讼卷宗合集\n核心业务意义（区别于128K的质变） 取消分片与RAG：不用拆分文档、不用向量检索分片，一次性载入全部资料做跨章节、跨文件全局推理； 全局关联推理：模型能同时记住数百份文档里的全部条款、变量、业务规则，自动交叉校验矛盾信息； 完整工程级代码处理：读取整个前后端代码库，一次性梳理全项目架构、定位跨文件Bug、重构整体逻辑； 长周期Agent原生支持：上万轮复杂多步骤任务（数据分析、方案推演、多智能体协同）全程不丢失历史指令； 垂直行业质变： 法律：全套案件证据、判决书、合同一次性对比找漏洞； 金融：全年财报、数百份公告联合建模； 研发：完整项目技术手册+代码一体化重构。 短板 推理显存占用高、速度慢、价格高于128K档位，简单业务使用会造成算力浪费。\n5）超百万窗口：2M–10M（实验室/企业定制，窄场景专用） 代表规格 Gemini 1.5/3系列2M、Llama4实验版10M、GPT-5.6旗舰150万\n适用场景 4小时长视频全文解析、小型图书馆多本专著联合研究、百万行超大型代码仓库、数年历史全量运营日志\n行业意义 仅头部科研、大型政企定制使用；硬件门槛极高，普通开发者/中小企业无实用价值，短期不会下放普惠。\n三、超长上下文（1M+）未来3年核心技术趋势（2026–2029） 1. 窗口规模：不再盲目堆长度，走向「实用最优区间」 短期（2026下半年–2027） 旗舰标配：1M–2M Token，兼顾成本与推理精度，2M成为企业通用黄金窗口； 中端商用模型全面下放512K–1M，长上下文取消溢价，不再是付费专属能力； 端侧分层：手机/PC本地常驻100K–256K，云端按需拉起1M完整上下文，端云协同常态化。 中长期（2028–2029） 实验室/定制模型突破5M–10M，面向大型代码库、数年全量日志、多卷丛书； 行业共识：单纯堆千万级上下文性价比极低，不会成为大众标配，仅政企、科研定制使用。 底层架构变化 MoE稀疏、SSM状态空间、滑动窗口、弹性KV缓存成为标配，解决长文本显存爆炸问题； 存算分离、KV缓存池化普及，大幅降低1M并发推理硬件门槛，云厂商长文本成本持续腰斩。 2. 技术范式：RAG不会消失，但行业架构彻底重构 朴素分片RAG衰退 1M窗口普及后，单份/少量关联文档场景可直接全量载入，不再需要切片、向量检索，传统简单RAG工具价值萎缩。 新型智能体RAG（Agentic RAG）成为主流组合方案 超大规模私有知识库（千万页级）、实时动态数据、外部数据库、实时业务系统仍依赖检索； 形成「原生长上下文 + 智能体检索记忆」混合架构：短期资料全量喂入，长期历史数据外部分层存储，模型自主选择载入原文/摘要/精简片段，弹性上下文管理普及。 上下文工程取代传统提示词工程 核心能力从写简短Prompt，转向长文档排序、信息权重控制、历史记忆分层、冲突信息校验，衍生全新岗位与工具链。 3. 能力融合：超长上下文+Agent+多模态三位一体 Agent天然依赖百万级全局记忆：复杂多步骤任务、跨天多轮流程、多智能体协同，必须完整留存全部指令与中间结果； 多模态超长上下文打通：长视频转录+配套图纸+合同+代码一次性联合推理，视频/音频长文本统一纳入1M窗口处理； 记忆分层标准化：短期上下文（1M模型原生窗口）、中期结构化记忆（向量库）、长期持久记忆（企业私有数据库）三层架构成为标准设计。 4. 成本与普惠趋势 2027年：1M Token推理单价降至当前128K水平，中小企业可无负担使用； 开源70B+模型出厂标配1M，本地单机多卡即可稳定部署，私有化部署门槛大幅降低； 轻量化长上下文小模型出现：10B–30B参数支持256K–512K，适配本地离线终端、边缘设备。 5. 行业底层变革：从「分段处理」到「全局一次性推理」 过去AI工作流：文档拆分→向量入库→检索片段→分段问答； 未来标准工作流：批量上传全部关联资料→一次性载入百万上下文→全局交叉推理、自动校验矛盾、输出完整结论； 直接压缩80%以上文档处理中间流程，重构法律、研发、金融、咨询、运维全行业工作模式。\n四、分维度可抓住的商业/职业/技术机会 （一）技术开发者 \u0026amp; 工程师机会（短期见效） 1. 长上下文工程落地开发（刚需岗位爆发） 核心需求：1M上下文模型适配、KV缓存优化、弹性记忆编排、端云长文本协同； 细分赛道： 1）企业私有长文档解析系统（律所、投行、设计院）； 2）完整代码仓库智能审查、全局架构重构工具； 3）长视频/会议录音批量智能分析平台； 差异化壁垒：掌握长上下文精度调优、遗忘问题修复、并发显存调度，普通RAG工程师无法替代。 2. 新一代混合架构产品（长上下文+Agentic RAG） 不再二选一，做混合系统：\n小体量资料直接全量进上下文； 海量历史数据、实时业务数据走智能体检索； 自动区分何时加载完整原文、何时使用摘要记忆，降低算力消耗； 适合独立开发者、小型AI创业团队做垂直SaaS。 3. 长上下文基础设施工具赛道 KV缓存池化、分布式长推理调度工具； 文档预处理专用工具：超长PDF/扫描件批量结构化、跨文件关联索引； 弹性上下文记忆管理框架（对标LongCat、ACE弹性编排），开源框架二次开发变现。 4. 私有化本地长模型部署服务 很多政企、金融、医疗数据不能上公有云，需求：本地70B+开源1M模型轻量化部署、量化优化、多卡分布式推理，外包服务溢价高。\n（二）垂直行业商业化机会（高毛利、低竞争） 1. 法律行业（百万上下文最强落地场景） 痛点：并购全套合同、诉讼卷宗、判例合集动辄几十万字，人工跨文档比对周期极长； 产品形态：\n律所SaaS：批量上传全套证据、合同、判决书，全局识别矛盾条款、隐藏风险、自动生成完整法律意见书； 企业合规年度批量合同审查； 盈利模式：按文档页数年费订阅，单客户年ARPU 5000–20000元。 2. 软件研发 \u0026amp; 代码智能工具 完整代码仓库一次性载入，全局架构梳理、跨文件Bug定位、全项目重构、接口统一校验； 对标Cursor升级：支持百万行代码全局分析，面向企业研发团队做私有化代码智能平台，付费意愿极强。\n3. 金融投研、投行尽调 全套财报、公告、行业研报、历史交易记录联合分析，自动交叉验证财务数据矛盾、行业长期趋势推演； 面向券商、资管、一级市场机构，B端定制化服务。\n4. 工业/工程设计院 全套图纸说明、技术规范、招投标文件、施工日志百万字联合解析，自动核查规范冲突、提取施工风险点。\n5. 医疗病案、学术科研 完整多年连续病案、全套实验论文一次性汇总分析，辅助科研综述、病例对比，适合医院、高校科研平台。\n（三）个人职业能力机会（普通人可快速布局） 差异化AI工程师核心竞争力 市面上90%开发者只会基础RAG，精通百万上下文工程、长距离召回优化、KV性能调优属于稀缺人才，大厂、AI创业公司高薪扩招； 垂直行业AI解决方案专家 法律、金融、研发领域，同时懂行业业务+长上下文落地，属于复合型稀缺人才，可独立承接项目、做自由顾问； 产品经理新赛道：超长上下文AI产品设计 传统对话产品思维失效，需要设计全局文档上传、跨文件对比、分层记忆、长任务Agent流程，企业急需懂长上下文的产品； 内容/咨询从业者工具升级 律师、咨询师、研究员、产品运营，掌握1M上下文工具，单人处理资料效率提升5–10倍，形成个人职场壁垒。 （四）长期底层赛道机会（2027后红利持续释放） 企业全量数据资产数字化服务 大量企业历史文档、合同、日志、代码杂乱无章，先做结构化清洗，再基于百万上下文模型搭建企业专属全局知识库； 端侧长上下文硬件+软件一体化 本地PC、工业终端、高性能工作站内置轻量化512K–1M模型，离线处理涉密长文档； 多模态超长一体化平台 长视频+音频转录+配套文档统一解析，面向教育、培训、媒体、政企会议纪要场景。 五、两类人群行动路线建议 1. 技术开发者（3个月落地路线） 吃透1M开源底座：DeepSeek V4、Qwen3、GLM-5长上下文版本本地部署； 掌握核心技术：KV缓存优化、滑动窗口、弹性记忆、混合RAG架构； 落地垂直最小产品：代码审查工具/合同解析工具，形成可演示案例； 求职/接单：主打「长上下文落地」差异化标签，避开内卷普通RAG岗位。 2. 行业从业者（律师/研发/金融/咨询） 熟练使用支持1M的商用模型（Claude、DeepSeek、国产百万上下文API）； 重构个人工作流：不再拆分文档，一次性全量导入全局分析； 打造行业专属Prompt库与文档处理流程，形成个人效率壁垒； 向企业输出AI数字化方案，转型内部AI负责人或独立行业AI顾问。 六、关键风险提醒（避坑） 不要单纯做通用长文本问答工具，无行业壁垒极易内卷； 不要低估显存成本：1M并发推理硬件门槛高，优先做B端私有化、按量付费模式； 不要完全抛弃RAG：超大知识库、实时动态数据场景，混合架构才是最优解； 纯堆上下文长度无价值，核心价值是全局跨文档关联推理，所有产品必须围绕该核心设计。 ","permalink":"https://lv-blog.pages.dev/posts/programming/ai/%E6%BC%AB%E8%B0%88ai%E4%B8%8A%E4%B8%8B%E6%96%87%E9%95%BF%E5%BA%A6/","summary":"\u003cpre class=\"mermaid\"\u003eflowchart TD\n    A[2018–2021\u003cbr/\u003e512–2K Token\u003cbr/\u003e金鱼记忆时代] --\u003e|只能处理短文、短句、单轮问答| B[2022–2023\u003cbr/\u003e4K–128K Token\u003cbr/\u003e中上下文普及时代]\n    B --\u003e|可处理论文、合同、小说、中型代码库\u003cbr/\u003eRAG 成为企业标准方案| C[2024–2025\u003cbr/\u003e200K–1M Token\u003cbr/\u003e百万上下文实验期]\n    C --\u003e|纸面支持 1M，但遗忘、成本、速度问题明显\u003cbr/\u003e仅限量 Beta| D[2026–2027\u003cbr/\u003e1M–2M Token\u003cbr/\u003e1M 量产普惠时代]\n    D --\u003e|1M 成为旗舰标配\u003cbr/\u003e可处理整卷宗、整代码库、全年业务日志| E[2028–2029\u003cbr/\u003e2M–10M Token\u003cbr/\u003e超百万窗口分化时代]\n    E --\u003e|2M 以上面向行业定制\u003cbr/\u003e长视频、大型代码库、多年日志联合分析| F[2030+\u003cbr/\u003e10M+ 弹性记忆\u003cbr/\u003e原生长记忆智能体时代]\n\u003c/pre\u003e\n\u003ch1 id=\"一大模型上下文完整进化时间线20182026分4个时代\"\u003e一、大模型上下文完整进化时间线（2018–2026，分4个时代）\u003c/h1\u003e\n\u003ch2 id=\"1-初代transformer时代5122k20182021金鱼记忆\"\u003e1. 初代Transformer时代：512–2K（2018–2021，金鱼记忆）\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e2018 GPT-1 / BERT：\u003cstrong\u003e512 token\u003c/strong\u003e，仅短文、短句分类，多轮对话必丢前文\u003c/li\u003e\n\u003cli\u003e2019 GPT-2：\u003cstrong\u003e1024 token\u003c/strong\u003e，能写短篇故事，连续对话3–5轮就遗忘历史\u003c/li\u003e\n\u003cli\u003e2020 GPT-3：\u003cstrong\u003e2048 token（2K）\u003c/strong\u003e，行业通用标准，仅支持短提示词、少量示例，长文档必须分段/摘要处理\u003c/li\u003e\n\u003cli\u003e2021 早期国产（文心一言初代、GLM-1）：统一2K上限，无长文本能力\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e时代特征\u003c/strong\u003e：没有原生长文本能力，所有长内容必须靠人工拆分、外部摘要，RAG雏形出现。\u003c/p\u003e\n\u003ch2 id=\"2-中上下文普及时代4k128k20222023工业化可用\"\u003e2. 中上下文普及时代：4K–128K（2022–2023，工业化可用）\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e2022 ChatGPT（GPT-3.5）：\u003cstrong\u003e4K\u003c/strong\u003e；年末升级16K，日常聊天够用，长文档仍吃力\u003c/li\u003e\n\u003cli\u003e2023.3 GPT-4 首发：\u003cstrong\u003e8K\u003c/strong\u003e，专业文档、代码单文件处理门槛打开\u003c/li\u003e\n\u003cli\u003e2023.7 Claude 2：\u003cstrong\u003e100K\u003c/strong\u003e，首个量产十万级窗口，法律、长篇论文场景爆发\u003c/li\u003e\n\u003cli\u003e2023.11 GPT-4 Turbo：\u003cstrong\u003e128K\u003c/strong\u003e，全球主流商用标配门槛，单本小说完整载入\u003c/li\u003e\n\u003cli\u003e2023年底 国产跟进：通义、GLM、混元开放32K/64K；开源Llama 2、Qwen 7B固定4K–32K\u003c/li\u003e\n\u003cli\u003e2023.12 Gemini 1.0 Ultra 纸面宣称1M，但仅实验室封闭测试，无法商用落地\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e时代里程碑\u003c/strong\u003e：128K成为专业AI标配；RAG成为行业标准方案，弥补窗口不足。\u003c/p\u003e\n\u003ch2 id=\"3-百万上下文实验期200k2m纸面20242025纸面强实际弱\"\u003e3. 百万上下文实验期：200K–2M纸面（2024–2025，纸面强、实际弱）\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e2024.2 Gemini 1.5 Pro：\u003cstrong\u003e原生2M token\u003c/strong\u003e，全球首个公开百万级模型，但存在严重「中间遗忘Lost in the Middle」，后半段文档召回暴跌，仅适合摘要，不适合精细推理，仅限开发者限量Beta\u003c/li\u003e\n\u003cli\u003e2024 Claude 3 Opus：\u003cstrong\u003e200K\u003c/strong\u003e，稳定可靠，法律行业主力，无严重遗忘问题\u003c/li\u003e\n\u003cli\u003e2025 上半年：各大厂商放出1M Beta（Claude Sonnet 4、Gemini 2.0），长上下文计费溢价极高、响应慢、显存开销巨大，企业少量试用，普通用户无法接触\u003c/li\u003e\n\u003cli\u003e2025 国产开源：Qwen、DeepSeek推出64K–128K底座，少量实验版支持256K\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e时代特征\u003c/strong\u003e：1M只是技术噱头，\u003cstrong\u003e标称≠有效\u003c/strong\u003e；硬件成本、遗忘问题、价格三重门槛，无法大规模落地。\u003c/p\u003e","title":"漫谈AI上下文长度"},{"content":"1. 悲观锁 vs 乐观锁（心态问题） 悲观锁：觉得肯定有人抢厕所，进去就反锁门（加锁），用完再开。\n→ Java里的 synchronized 和 ReentrantLock 都是这种。安全，但慢。\n乐观锁：觉得没人抢，不锁门，但上厕所时盯着门把手（版本号/时间戳），如果发现被人动过（冲突），就重试。\n→ Java里的 CAS（比较并交换） 就是这种，比如 AtomicInteger。快，但冲突多时会反复重试。\n2. 公平锁 vs 非公平锁（排队问题） 公平锁：先来后到，乖乖排队。\n→ new ReentrantLock(true)。公平，但效率低（大家都得排队）。\n非公平锁：新来的可以插队，如果锁刚好释放，它就直接抢。\n→ synchronized 和默认的 ReentrantLock 都是非公平。效率高，但可能导致某些线程饿死（一直抢不到）。\n3. 可重入锁（同一个人的多次进入） 你进了厕所A，发现里面还有个小隔间B，你能直接进B，不用再掏钥匙。\n→ synchronized 和 ReentrantLock 都支持。同一个线程可以多次获取同一把锁，防止自己把自己卡死。 4. 读写锁（分情况管理） 厕所分蹲位（写锁）和洗手池（读锁）。 多人同时洗手（读）没问题。 但有人蹲坑（写）时，别人既不能蹲也不能洗手（全阻塞）。\n→ ReentrantReadWriteLock。适合读多写少的场景。 5. 共享锁 vs 排他锁（权限级别） 排他锁（写锁）：厕所门一锁，谁也别进。 共享锁（读锁）：可以多人同时看同一份文件（但不能改）。\n→ 读写锁就是共享/排他的典型实现。 6. 偏向锁 → 轻量级锁 → 重量级锁（锁升级，JVM自动优化） 这是 synchronized 的底层升级过程（为了性能）：\n偏向锁：厕所只认你一个人，你每次来都不用掏钥匙（无竞争）。 轻量级锁：偶尔有别人来，你们用“自旋”方式（原地转圈等）抢，不挂起线程（省资源）。 重量级锁：竞争激烈，排队挂起（操作系统介入，慢）。\n→ 这是JVM自动做的，你不用管，但知道它存在即可。 7. 自旋锁（不睡觉，死等） 厕所被占，你不去排队睡觉，而是在门口原地转圈（循环检查），等它释放。\n→ 适合持有锁时间很短的情况，避免线程挂起/唤醒的开销。Java里的 CAS 就是自旋思想。 8. 分段锁（分块管理） 一个大厕所分成多个小隔间，锁只锁其中一间，不影响其他间。\n→ ConcurrentHashMap 早期就是用分段锁，提升并发度（现在改用CAS+细粒度锁了）。 一张图总结（按使用场景选）： 场景 推荐锁 简单同步，代码少 synchronized 需要可中断、超时、公平等灵活功能 ReentrantLock 读多写少（如缓存） ReentrantReadWriteLock 计数器、自增等简单操作 AtomicXXX（乐观锁） 追求极致性能，竞争不激烈 偏向锁/轻量级锁（JVM自动） ","permalink":"https://lv-blog.pages.dev/posts/programming/%E4%B8%80%E4%B8%AA%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BA%94%E8%AF%A5%E7%9F%A5%E9%81%93%E7%9A%84%E5%90%84%E7%A7%8D%E9%94%81/","summary":"\u003ch3 id=\"1-悲观锁-vs-乐观锁心态问题\"\u003e1. 悲观锁 vs 乐观锁（心态问题）\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e悲观锁\u003c/strong\u003e：觉得肯定有人抢厕所，进去就\u003cstrong\u003e反锁门\u003c/strong\u003e（加锁），用完再开。\u003cbr\u003e\n→ Java里的 \u003ccode\u003esynchronized\u003c/code\u003e 和 \u003ccode\u003eReentrantLock\u003c/code\u003e 都是这种。\u003cstrong\u003e安全，但慢\u003c/strong\u003e。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e乐观锁\u003c/strong\u003e：觉得没人抢，不锁门，但上厕所时\u003cstrong\u003e盯着门把手\u003c/strong\u003e（版本号/时间戳），如果发现被人动过（冲突），就重试。\u003cbr\u003e\n→ Java里的 \u003cstrong\u003eCAS（比较并交换）\u003c/strong\u003e 就是这种，比如 \u003ccode\u003eAtomicInteger\u003c/code\u003e。\u003cstrong\u003e快，但冲突多时会反复重试\u003c/strong\u003e。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch3 id=\"2-公平锁-vs-非公平锁排队问题\"\u003e2. 公平锁 vs 非公平锁（排队问题）\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e公平锁\u003c/strong\u003e：先来后到，乖乖排队。\u003cbr\u003e\n→ \u003ccode\u003enew ReentrantLock(true)\u003c/code\u003e。\u003cstrong\u003e公平，但效率低\u003c/strong\u003e（大家都得排队）。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e非公平锁\u003c/strong\u003e：新来的可以\u003cstrong\u003e插队\u003c/strong\u003e，如果锁刚好释放，它就直接抢。\u003cbr\u003e\n→ \u003ccode\u003esynchronized\u003c/code\u003e 和默认的 \u003ccode\u003eReentrantLock\u003c/code\u003e 都是非公平。\u003cstrong\u003e效率高，但可能导致某些线程饿死\u003c/strong\u003e（一直抢不到）。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch3 id=\"3-可重入锁同一个人的多次进入\"\u003e3. 可重入锁（同一个人的多次进入）\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e你进了厕所A，发现里面还有个小隔间B，你\u003cstrong\u003e能直接进B\u003c/strong\u003e，不用再掏钥匙。\u003cbr\u003e\n→ \u003ccode\u003esynchronized\u003c/code\u003e 和 \u003ccode\u003eReentrantLock\u003c/code\u003e 都支持。\u003cstrong\u003e同一个线程可以多次获取同一把锁\u003c/strong\u003e，防止自己把自己卡死。\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch3 id=\"4-读写锁分情况管理\"\u003e4. 读写锁（分情况管理）\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e厕所分\u003cstrong\u003e蹲位（写锁）\u003cstrong\u003e和\u003c/strong\u003e洗手池（读锁）\u003c/strong\u003e。\n\u003cul\u003e\n\u003cli\u003e多人同时洗手（读）没问题。\u003c/li\u003e\n\u003cli\u003e但有人蹲坑（写）时，别人既不能蹲也不能洗手（全阻塞）。\u003cbr\u003e\n→ \u003ccode\u003eReentrantReadWriteLock\u003c/code\u003e。\u003cstrong\u003e适合读多写少\u003c/strong\u003e的场景。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch3 id=\"5-共享锁-vs-排他锁权限级别\"\u003e5. 共享锁 vs 排他锁（权限级别）\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e排他锁（写锁）\u003c/strong\u003e：厕所门一锁，谁也别进。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e共享锁（读锁）\u003c/strong\u003e：可以多人同时看同一份文件（但不能改）。\u003cbr\u003e\n→ 读写锁就是共享/排他的典型实现。\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch3 id=\"6-偏向锁--轻量级锁--重量级锁锁升级jvm自动优化\"\u003e6. 偏向锁 → 轻量级锁 → 重量级锁（锁升级，JVM自动优化）\u003c/h3\u003e\n\u003cp\u003e这是 \u003ccode\u003esynchronized\u003c/code\u003e 的底层升级过程（为了性能）：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e偏向锁\u003c/strong\u003e：厕所只认你一个人，你每次来都不用掏钥匙（无竞争）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e轻量级锁\u003c/strong\u003e：偶尔有别人来，你们用“自旋”方式（原地转圈等）抢，不挂起线程（省资源）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e重量级锁\u003c/strong\u003e：竞争激烈，排队挂起（操作系统介入，慢）。\u003cbr\u003e\n→ 这是JVM自动做的，\u003cstrong\u003e你不用管\u003c/strong\u003e，但知道它存在即可。\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch3 id=\"7-自旋锁不睡觉死等\"\u003e7. 自旋锁（不睡觉，死等）\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e厕所被占，你不去排队睡觉，而是\u003cstrong\u003e在门口原地转圈\u003c/strong\u003e（循环检查），等它释放。\u003cbr\u003e\n→ 适合\u003cstrong\u003e持有锁时间很短\u003c/strong\u003e的情况，避免线程挂起/唤醒的开销。Java里的 \u003ccode\u003eCAS\u003c/code\u003e 就是自旋思想。\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch3 id=\"8-分段锁分块管理\"\u003e8. 分段锁（分块管理）\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e一个大厕所分成多个小隔间，锁只锁其中一间，不影响其他间。\u003cbr\u003e\n→ \u003ccode\u003eConcurrentHashMap\u003c/code\u003e 早期就是用分段锁，提升并发度（现在改用CAS+细粒度锁了）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch3 id=\"一张图总结按使用场景选\"\u003e一张图总结（按使用场景选）：\u003c/h3\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e场景\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e推荐锁\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e简单同步，代码少\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003esynchronized\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e需要可中断、超时、公平等灵活功能\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eReentrantLock\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e读多写少（如缓存）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eReentrantReadWriteLock\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e计数器、自增等简单操作\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eAtomicXXX\u003c/code\u003e（乐观锁）\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e追求极致性能，竞争不激烈\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e偏向锁/轻量级锁（JVM自动）\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e","title":"一个程序员应该知道的各种锁"},{"content":"Kafka Kafdrop services: kafdrop: image: obsidiandynamics/kafdrop container_name: kafdrop ports: - \u0026#34;9000:9000\u0026#34; environment: KAFKA_BROKERCONNECT: \u0026#34;localhost:9092\u0026#34; SERVER_SERVLET_CONTEXTPATH: \u0026#34;/\u0026#34; restart: unless-stopped KRaft 模式 services: kafka: image: apache/kafka:3.9.0 container_name: kafka hostname: kafka ports: - \u0026#34;9092:9092\u0026#34; environment: KAFKA_NODE_ID: 1 KAFKA_PROCESS_ROLES: broker,controller KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 # 注意：这里不要换行，不要逗号 KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093 # 外部访问地址 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 CLUSTER_ID: MkU3OEVBNTcwNTJENDM2Qk volumes: - ./data:/var/lib/kafka/data restart: unless-stopped ","permalink":"https://lv-blog.pages.dev/posts/programming/backend/code-snippets/docker-%E5%B8%B8%E7%94%A8%E9%85%8D%E7%BD%AE/","summary":"\u003ch2 id=\"kafka\"\u003eKafka\u003c/h2\u003e\n\u003ch3 id=\"kafdrop\"\u003eKafdrop\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003eservices\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003ekafdrop\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003eimage\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eobsidiandynamics/kafdrop\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003econtainer_name\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003ekafdrop\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003eports\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      - \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;9000:9000\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003eenvironment\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003eKAFKA_BROKERCONNECT\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;localhost:9092\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003eSERVER_SERVLET_CONTEXTPATH\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003erestart\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eunless-stopped\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"kraft-模式\"\u003eKRaft 模式\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003eservices\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003ekafka\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003eimage\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eapache/kafka:3.9.0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003econtainer_name\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003ekafka\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003ehostname\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003ekafka\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003eports\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      - \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;9092:9092\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003eenvironment\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003eKAFKA_NODE_ID\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003eKAFKA_PROCESS_ROLES\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003ebroker,controller\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003eKAFKA_CONTROLLER_QUORUM_VOTERS\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e@\u003cspan style=\"color:#ae81ff\"\u003ekafka:9093\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#75715e\"\u003e# 注意：这里不要换行，不要逗号\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003eKAFKA_LISTENERS\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003ePLAINTEXT://:9092,CONTROLLER://:9093\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#75715e\"\u003e# 外部访问地址\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003eKAFKA_ADVERTISED_LISTENERS\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003ePLAINTEXT://localhost:9092\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003eKAFKA_LISTENER_SECURITY_PROTOCOL_MAP\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003ePLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003eKAFKA_CONTROLLER_LISTENER_NAMES\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eCONTROLLER\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003eKAFKA_INTER_BROKER_LISTENER_NAME\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003ePLAINTEXT\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003eKAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003eKAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003eKAFKA_TRANSACTION_STATE_LOG_MIN_ISR\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003eCLUSTER_ID\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eMkU3OEVBNTcwNTJENDM2Qk\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003evolumes\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      - \u003cspan style=\"color:#ae81ff\"\u003e./data:/var/lib/kafka/data\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003erestart\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eunless-stopped\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"Docker 常用配置"},{"content":"在 Java 的世界里，“分代年龄”（Age） 是专门为 JVM 垃圾回收（GC）设计的一个计数器。\n简单来说，它记录了一个 Java 对象在垃圾回收中活过了多少轮。就像打游戏刷副本一样，对象每在垃圾回收的“清洗”中幸存下来一次，它的分代年龄就会 +1。\n1. 为什么需要“分代年龄”？ 这源于 Java 垃圾回收领域著名的 弱分代假说（Weak Generational Hypothesis）：\n绝大多数的 Java 对象都是“朝生夕死”的。 比如你在一个方法里 new 了一个局部变量，方法结束了，这个对象就没用了。但也有极少数对象（如 Spring 的 Bean、数据库连接池）会一直存活。\n为了高效管理内存，JVM 把堆内存分成了两大区域：\n新生代（Young Generation）： 存放刚出生、寿命短的对象。 老年代（Old Generation）： 存放寿命长、常驻内存的对象。 “分代年龄”就是对象从“新生代”晋升到“老年代”的资格证和进度条。\n2. 它的工作流程是怎样的？ 我们可以把新生代想象成一个“新手村”，老年代想象成“满级主城”：\n出生（0岁）： 绝大多数新对象在新生代的 Eden（伊甸园）区 出生。 第一次渡劫（1岁）： 发生了一次 Minor GC（新生代垃圾回收）。如果这个对象没被回收，它会幸存下来，被移动到 Survivor（幸存者）区，同时它的分代年龄变成 1 岁。 继续熬资历（每活过一轮 +1岁）： 以后每发生一次 Minor GC，只要它还在幸存者区折腾且没死掉，它的年龄就会加 1 岁。 晋升老年代： 当它的年龄达到一定阈值（默认是 15 岁，由 JVM 参数 -XX:MaxTenuringThreshold 决定）时，JVM 就会认为：“这小伙子挺能活，应该是个核心常驻对象。” 于是把它晋升（Promote）到老年代。 3. 它存在对象的什么地方？ 正如我们在讨论“锁”时提到的，分代年龄存在于每个 Java 对象的 对象头（Object Header）的 Mark Word 里面。\n在 HotSpot 虚拟机中，JVM 只给分代年龄分配了 4 个比特位（4 bits） 的空间。\n💡 为什么默认上限是 15 岁？ 因为 4 个比特位能表示的最大二进制数是 1111，换算成十进制就是 15。所以，你无论怎么调整 JVM 参数，分代年龄的最大值也只能设为 15。\n万一这个对象熬到了 15 岁，刚进老年代，程序就再也不用它了怎么办？\n这种情况在实际开发中不仅存在，而且非常普遍。这种在老年代中死掉、但还没被清理的对象，在 JVM 中被称为“浮动垃圾”（Floating Garbage）或“积压垃圾”。\nJVM 早就预料到了这种“看错人”的情况，并准备了一套完整的应对机制。\n4. 最终兜底：Full GC（老年代垃圾回收） 老年代虽然被称为“养老院”，但它绝对不是“垃圾填埋场”。\n当进入老年代的垃圾越来越多，导致老年代的空间快要被装满时，JVM 就会触发一次长痛的Full GC（或者 Major GC）。\n做法： 这是一次全堆大扫除。垃圾回收器会把老年代里那些“占着茅坑不拉屎”（已经不再被引用的）假长寿对象全部揪出来，统统清理掉。 代价： Full GC 的成本很高，它通常会触发 STW（Stop-The-World），让整个应用程序短暂停顿。因此，JVM 的调优目标之一就是尽量减少 Full GC 的发生。 5. 现代 GC 的智能“止损”：G1 和 ZGC 传统的垃圾回收器（如 CMS）确实要等老年代满了才被动去扫除。但现代 Java（Java 9 之后的 G1，以及 Java 11/17 引入的 ZGC）聪明得多，它们不再死板地看“分代年龄”。\n划分逻辑区域（Region） 现代 GC 把整个堆内存拆成了几百甚至上千个小格子（Regions）。\n哪怕一个对象年纪很大，掉进了一个已经全是垃圾的格子（Region）里。 G1 垃圾回收器会进行收益评估：“这个格子里 95% 都是垃圾，清理它性价比极高！” 于是，G1 会在不触发 Full GC 的情况下，顺手把这个格子里的“老年代垃圾”给回收掉。 6. 开发者应该反思什么？（避免“过早晋升”） 如果你的代码里频繁出现“到了老年代就不用了”的对象，通常意味着程序出现了“过早晋升”（Premature Promotion）。\n常见的原因有两个：\n原因一：新手村（Survivor 区）太小了 场景： 发生 Minor GC 时，存活的对象太多，Survivor 区装不下了。 结果： 此时 JVM 会触发担保机制，一些只有 1 岁、2 岁的年轻对象，被迫直接“偷渡”到老年代。 解决办法： 增大堆内存，或者调整参数（如 -XX:SurvivorRatio），让新手村大一点，把他们尽量憋在新生代里杀掉。 原因二：代码里有大对象（比如大数组、巨型字符串） 场景： 你在代码里 new 了一个 10MB 的字节数组，用来做临时文件读取。 结果： 这种大对象，JVM 连新手村都不让进，直接出生在老年代。读取完文件后，它瞬间就成了老年代里的废品。 解决办法： 避免一次性加载海量数据，改用流式处理（Stream），或者复用对象池。 总结 万一对象到了老年代才死，JVM 依然能通过 老年代大扫除（Full GC） 或者 现代 GC 的区域轮询（G1/ZGC） 把它清理掉。\n但这是一种“性能损耗”，作为开发者，我们写的代码最好让“朝生夕死”的对象在新生代就被干掉，尽量别去惊动老年代。\n","permalink":"https://lv-blog.pages.dev/posts/programming/why-why-why/java-gc%E6%98%AF%E5%A6%82%E4%BD%95%E9%A9%B1%E8%B5%B6%E8%80%81%E5%B9%B4%E5%AF%B9%E8%B1%A1%E7%9A%84/","summary":"\u003cp\u003e在 Java 的世界里，\u003cstrong\u003e“分代年龄”（Age）\u003c/strong\u003e 是专门为 JVM 垃圾回收（GC）设计的一个\u003cstrong\u003e计数器\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e简单来说，它记录了\u003cstrong\u003e一个 Java 对象在垃圾回收中活过了多少轮\u003c/strong\u003e。就像打游戏刷副本一样，对象每在垃圾回收的“清洗”中幸存下来一次，它的分代年龄就会 \u003cstrong\u003e+1\u003c/strong\u003e。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-为什么需要分代年龄\"\u003e1. 为什么需要“分代年龄”？\u003c/h2\u003e\n\u003cp\u003e这源于 Java 垃圾回收领域著名的 \u003cstrong\u003e弱分代假说（Weak Generational Hypothesis）\u003c/strong\u003e：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e绝大多数的 Java 对象都是“朝生夕死”的。\u003c/strong\u003e 比如你在一个方法里 new 了一个局部变量，方法结束了，这个对象就没用了。但也有极少数对象（如 Spring 的 Bean、数据库连接池）会一直存活。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e为了高效管理内存，JVM 把堆内存分成了两大区域：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e新生代（Young Generation）：\u003c/strong\u003e 存放刚出生、寿命短的对象。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e老年代（Old Generation）：\u003c/strong\u003e 存放寿命长、常驻内存的对象。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e“分代年龄”就是对象从“新生代”晋升到“老年代”的资格证和进度条。\u003c/strong\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"2-它的工作流程是怎样的\"\u003e2. 它的工作流程是怎样的？\u003c/h2\u003e\n\u003cp\u003e我们可以把新生代想象成一个“新手村”，老年代想象成“满级主城”：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e出生（0岁）：\u003c/strong\u003e 绝大多数新对象在新生代的 \u003cstrong\u003eEden（伊甸园）区\u003c/strong\u003e 出生。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e第一次渡劫（1岁）：\u003c/strong\u003e 发生了一次 Minor GC（新生代垃圾回收）。如果这个对象没被回收，它会幸存下来，被移动到 \u003cstrong\u003eSurvivor（幸存者）区\u003c/strong\u003e，同时它的分代年龄变成 \u003cstrong\u003e1 岁\u003c/strong\u003e。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e继续熬资历（每活过一轮 +1岁）：\u003c/strong\u003e 以后每发生一次 Minor GC，只要它还在幸存者区折腾且没死掉，它的年龄就会加 1 岁。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e晋升老年代：\u003c/strong\u003e 当它的年龄达到一定阈值（\u003cstrong\u003e默认是 15 岁\u003c/strong\u003e，由 JVM 参数 \u003ccode\u003e-XX:MaxTenuringThreshold\u003c/code\u003e 决定）时，JVM 就会认为：“这小伙子挺能活，应该是个核心常驻对象。” 于是把它\u003cstrong\u003e晋升（Promote）到老年代\u003c/strong\u003e。\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr\u003e\n\u003ch2 id=\"3-它存在对象的什么地方\"\u003e3. 它存在对象的什么地方？\u003c/h2\u003e\n\u003cp\u003e正如我们在讨论“锁”时提到的，分代年龄存在于每个 Java 对象的 \u003cstrong\u003e对象头（Object Header）的 Mark Word\u003c/strong\u003e 里面。\u003c/p\u003e","title":"Java GC 是如何驱赶“老年群体”的"},{"content":"1. 为什么当初要这样设计？（设计初衷） Java 诞生于 20 世纪 90 年代中期，当时多线程编程（并发）正在兴起。Java 的设计者高斯林（James Gosling）希望 Java 成为一门简单易用的多线程语言。\n简化并发编程 如果对象没有内置锁，你每次想给一段代码加锁，都必须显式地创建一个 Lock 对象：\n// 如果没有内置锁，你得这么写： Lock myLock = new ReentrantLock(); void doSomething() { myLock.lock(); try { // 业务逻辑 } finally { myLock.unlock(); } } 而 Java 设计者引入了 synchronized 关键字，直接让对象充当锁，把复杂的同步简化成了：\n// 极简的同步写法 synchronized(this) { // 业务逻辑 } “万物皆对象，万物皆可为锁”的设计，让开发者不需要管理一堆乱七八糟的锁对象，只要拿到目标对象，就能直接进行同步控制。\n2. 这样不浪费内存吗？JVM 是怎么“偷懒”的？ 你担心的内存浪费，JVM 工程师早就想到了。他们绝对不会傻到给每个刚 new 出来的对象都分配一个沉重的操作系统级别的锁（Monitor）。\nJVM 解决这个问题的核心思路是：按需分配，动态升级。\n秘密武器：对象头（Object Header） 在 Java 中，每个对象的内存结构里都有一个叫 Mark Word（标记字） 的区域（在 64 位虚拟机上占 8 个字节 / 64 位）。\n这 8 个字节是并发的核心。它并不直接存储一个复杂的锁对象，而是像一个“多功能瑞士军刀”，根据锁的状态，复用这 64 位的存储空间：\n锁状态 Mark Word 存储的内容 解释 无锁状态 (001) 对象的 HashCode、分代年龄等 绝大多数 Java 对象的一生都是这个状态。 根本没有锁，完全不浪费额外内存。 偏向锁 (101) 持有锁的线程 ID、分代年龄 JVM 发现只有“你”这一个线程老来拿锁，干脆直接把你的名字写在对象头上。几乎没有性能和内存开销。 轻量级锁 (00) 指向当前线程栈中锁记录的指针 出现轻微竞争时，利用 CAS（自旋）把指针指向线程自己的栈，依然不需要向操作系统申请重锁。 重量级锁 (10) 指向 Monitor 对象（管程）的指针 只有当激烈竞争发生时，JVM 才会真正创建一个重量级的 Monitor 锁对象，并把指针存到这里。 💡 总结来说： \u0026gt; 只要你不用 synchronized，或者没有多线程激烈的抢夺，这个对象头里存的就只是 HashCode 和垃圾回收数据。锁的属性只是这 64 位数据在特定状态下的“临时兼职”，并没有为锁单独开辟额外的内存空间。\n3. 现代 Java 的反思：这种设计完美吗？ 虽然 JVM 优化到了极致，但“万物皆可为锁”的设计在今天看来，确实带有时代的局限性。\n语义模糊： 一个用来存数据的 User 对象，同时还能用来当锁，这违反了“单一职责原则”。 现代 Java 的转向： 自 Java 5 引入 java.util.concurrent（如 ReentrantLock）以来，官方更推荐显式锁。而在现代高并发开发中，大家已经很少直接在普通业务对象上用 synchronized 了。 ","permalink":"https://lv-blog.pages.dev/posts/programming/why-why-why/%E4%B8%BA%E4%BB%80%E4%B9%88%E6%AF%8F%E4%B8%AAjava%E5%AF%B9%E8%B1%A1%E9%83%BD%E6%9C%89%E9%94%81%E7%9A%84%E5%B1%9E%E6%80%A7%E8%BF%99%E6%A0%B7%E4%B8%8D%E6%98%AF%E6%B5%AA%E8%B4%B9%E5%86%85%E5%AD%98%E5%90%97/","summary":"\u003ch2 id=\"1-为什么当初要这样设计设计初衷\"\u003e1. 为什么当初要这样设计？（设计初衷）\u003c/h2\u003e\n\u003cp\u003eJava 诞生于 20 世纪 90 年代中期，当时多线程编程（并发）正在兴起。Java 的设计者高斯林（James Gosling）希望 Java 成为一门简单易用的多线程语言。\u003c/p\u003e\n\u003ch3 id=\"简化并发编程\"\u003e简化并发编程\u003c/h3\u003e\n\u003cp\u003e如果对象没有内置锁，你每次想给一段代码加锁，都必须显式地创建一个 \u003ccode\u003eLock\u003c/code\u003e 对象：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 如果没有内置锁，你得这么写：\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eLock myLock \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e ReentrantLock();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003edoSomething\u003c/span\u003e() {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    myLock.\u003cspan style=\"color:#a6e22e\"\u003elock\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003etry\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#75715e\"\u003e// 业务逻辑\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    } \u003cspan style=\"color:#66d9ef\"\u003efinally\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        myLock.\u003cspan style=\"color:#a6e22e\"\u003eunlock\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e而 Java 设计者引入了 \u003ccode\u003esynchronized\u003c/code\u003e 关键字，直接让对象充当锁，把复杂的同步简化成了：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 极简的同步写法\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003esynchronized\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003ethis\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 业务逻辑\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e“万物皆对象，万物皆可为锁”的设计，让开发者不需要管理一堆乱七八糟的锁对象，只要拿到目标对象，就能直接进行同步控制。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"2-这样不浪费内存吗jvm-是怎么偷懒的\"\u003e2. 这样不浪费内存吗？JVM 是怎么“偷懒”的？\u003c/h2\u003e\n\u003cp\u003e你担心的内存浪费，JVM 工程师早就想到了。他们绝对不会傻到给每个刚 new 出来的对象都分配一个沉重的操作系统级别的锁（Monitor）。\u003c/p\u003e\n\u003cp\u003eJVM 解决这个问题的核心思路是：\u003cstrong\u003e按需分配，动态升级。\u003c/strong\u003e\u003c/p\u003e\n\u003ch3 id=\"秘密武器对象头object-header\"\u003e秘密武器：对象头（Object Header）\u003c/h3\u003e\n\u003cp\u003e在 Java 中，每个对象的内存结构里都有一个叫 \u003cstrong\u003eMark Word（标记字）\u003c/strong\u003e 的区域（在 64 位虚拟机上占 \u003cstrong\u003e8 个字节 / 64 位\u003c/strong\u003e）。\u003c/p\u003e","title":"为什么每个java对象都有锁的属性？这样不是浪费内存吗"},{"content":"在 Java 后端开发面试或源码阅读中，HashMap 永远是避不开的核心。很多人能熟练地背出它的扩容阈值、红黑树化条件，但当深入到源码底层，看到诸如 e.hash \u0026amp; (newCap - 1) 和 e.hash \u0026amp; oldCap 这样的位运算时，往往会陷入沉思。\n一、 起源：为什么数组长度必须是 2 的次幂？ 在散列表中，为了让数据均匀分布，最直观的想法是对哈希值进行取模（求余数）：\n$$\\text{索引位置} = \\text{hash} \\pmod{\\text{Capacity}}$$然而，在 CPU 的底层底层执行中，除法和取模（%）是非常昂贵的算术操作（可能需要几十个时钟周期）。为了追求极致的性能，底层的数论定理为我们提供了一个完美的替代方案：\n数学定理：当容量 $C$ 是 $2$ 的次幂（$2^n$）时，对于任意整数 $H$，满足：\n$$H \\pmod C \\equiv H \\ \\\u0026 \\ (C - 1)$$ 位运算（\u0026amp;）在 CPU 中只需要 1 个时钟周期！为了享受到这个性能红利，HashMap 在源码中将容量死死限制为 2 的次幂：\n// 默认初始容量 16 (2的4次方) static final int DEFAULT_INITIAL_CAPACITY = 1 \u0026lt;\u0026lt; 4; 裁剪器的魔术：hash \u0026amp; (newCap - 1) 以容量 16 为例，16 - 1 = 15（二进制：0000 1111）。任何 Hash 值与 15 进行 \u0026amp; 运算，高位都会被无情“抹零”，只有最后 4 位被保留下来：\ne.hash : 1010 0101 (某个对象的Hash值) \u0026amp; 15 (16-1): 0000 1111 ------------------------------- 结果 : 0000 0101 (十进制的 5，即数组索引) 裁剪出来的结果范围绝对在 0 ~ 15 之间，在数学上完全等同于取模，速度却快了成百上千倍。\n二、 进阶：扩容时的核心优化 e.hash \u0026amp; oldCap 当 HashMap 触发扩容（resize()）时，原数组中的链表节点面临着重新分配（Rehash）。在 JDK 7 中，程序需要对每个节点重新进行一次上面提到的计算，不仅耗时，在多线程下还容易形成死循环。\nJDK 8 引入了一个极其巧妙的判断：(e.hash \u0026amp; oldCap) == 0。\n1. 搬家还是留守？ 扩容是翻倍的。比如旧容量 oldCap = 16（二进制 0001 0000），新容量 newCap = 32。\n容量为 16 时，看的是二进制的低 4 位。 容量为 32 时，看的是二进制的低 5 位。 也就是说，决定一个节点扩容后去哪里的，仅仅取决于它 Hash 值多出来的左边那 1 位（二进制的第 5 位）是 0 还是 1。\n2. 精准的按位照准 oldCap（16）的二进制刚好是 0001 0000，除了第 5 位是 1，其余全为 0。 因此，e.hash \u0026amp; oldCap 就像是一把精准的手电筒，只照看新增的这一位：\n结果 == 0：说明新增的那一位是 0。扩容后重新计算位置，它依然会留在原下标位置（归为低位链表）。 结果 != 0：说明新增的那一位是 1。扩容后它的新位置必然是 原下标 + oldCap（归为高位链表）。 假设节点 A 和 B 之前都在下标 5： 节点 A 的 Hash: ...0000 0101 -\u0026gt; A \u0026amp; 16 = 0 -\u0026gt; 留守原位（下标 5） 节点 B 的 Hash: ...0001 0101 -\u0026gt; B \u0026amp; 16 = 16 (非0) -\u0026gt; 搬家（5 + 16 = 下标 21） 三、 总结：HashMap 扩容全景流程 基于上述的高低位拆分优化，HashMap 的 put 与 resize 流程变得无比丝滑。\n1. Put 方法核心步骤 计算哈希：利用扰动函数 (h = key.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16) 将高位特征混合到低位。 定位桶位：通过 hash \u0026amp; (capacity - 1) 快速定位。 解决冲突：桶为空直接放入；不为空则遍历链表或红黑树。若链表长度 $\\ge 8$ 且数组长度 $\\ge 64$ 则触发树化。 检查扩容：当元素总量超过阈值（capacity * loadFactor）时，启动 resize()。 2. Resize 扩容数据迁移步骤 graph TD A[开始扩容 resize] --\u003e B[\"创建新数组 newTable, 容量翻倍\"] B --\u003e C[遍历旧数组的每个桶位] C --\u003e D{桶位节点类型?} D -- 单节点 --\u003e E[\"重新计算索引: e.hash \u0026 (newCap - 1) 放入新数组\"] D -- 红黑树 --\u003e F[调用 split 拆分树] D -- 链表 --\u003e G{\"判断 (e.hash \u0026 oldCap) == 0\"} G -- 是 --\u003e H[\"归入低位链表 位置不变: newTable[原位置]\"] G -- 否 --\u003e I[\"归入高位链表 搬家: newTable[原位置 + oldCap]\"] E --\u003e J{是否遍历完?} F --\u003e J H --\u003e J I --\u003e J J -- 否 --\u003e C J -- 是 --\u003e K[扩容完成] 结语：工程美学的最高体现 HashMap 的设计者 Joshua Bloch 等大牛，通过强制容器大小为 2 的次幂 这一空间规律，成功用 位运算 换取了时间的极致性能。\n从 \u0026amp; (cap - 1) 的精简截取，到 \u0026amp; oldCap 的高低位完美拆分，HashMap 的源码向我们展示了什么是真正的计算机工程美学——将严谨的数学定理，转化为压榨硬件底层的极致武器。\n","permalink":"https://lv-blog.pages.dev/posts/programming/why-why-why/%E8%AE%A1%E7%AE%97%E6%9C%BA%E5%A6%82%E4%BD%95%E6%B1%82%E6%A8%A1/","summary":"\u003cp\u003e在 Java 后端开发面试或源码阅读中，\u003ccode\u003eHashMap\u003c/code\u003e 永远是避不开的核心。很多人能熟练地背出它的扩容阈值、红黑树化条件，但当深入到源码底层，看到诸如 \u003ccode\u003ee.hash \u0026amp; (newCap - 1)\u003c/code\u003e 和 \u003ccode\u003ee.hash \u0026amp; oldCap\u003c/code\u003e 这样的位运算时，往往会陷入沉思。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"一-起源为什么数组长度必须是-2-的次幂\"\u003e一、 起源：为什么数组长度必须是 2 的次幂？\u003c/h2\u003e\n\u003cp\u003e在散列表中，为了让数据均匀分布，最直观的想法是对哈希值进行\u003cstrong\u003e取模（求余数）\u003c/strong\u003e：\u003c/p\u003e\n$$\\text{索引位置} = \\text{hash} \\pmod{\\text{Capacity}}$$\u003cp\u003e然而，在 CPU 的底层底层执行中，\u003cstrong\u003e除法和取模（%）是非常昂贵的算术操作\u003c/strong\u003e（可能需要几十个时钟周期）。为了追求极致的性能，底层的数论定理为我们提供了一个完美的替代方案：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e数学定理\u003c/strong\u003e：当容量 $C$ 是 $2$ 的次幂（$2^n$）时，对于任意整数 $H$，满足：\u003c/p\u003e\n$$H \\pmod C \\equiv H \\ \\\u0026 \\ (C - 1)$$\u003c/blockquote\u003e\n\u003cp\u003e位运算（\u003ccode\u003e\u0026amp;\u003c/code\u003e）在 CPU 中只需要 \u003cstrong\u003e1 个时钟周期\u003c/strong\u003e！为了享受到这个性能红利，HashMap 在源码中将容量死死限制为 2 的次幂：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 默认初始容量 16 (2的4次方)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003efinal\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e DEFAULT_INITIAL_CAPACITY \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e 1 \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u0026lt;\u003c/span\u003e 4;\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"裁剪器的魔术hash--newcap---1\"\u003e裁剪器的魔术：\u003ccode\u003ehash \u0026amp; (newCap - 1)\u003c/code\u003e\u003c/h3\u003e\n\u003cp\u003e以容量 \u003ccode\u003e16\u003c/code\u003e 为例，\u003ccode\u003e16 - 1 = 15\u003c/code\u003e（二进制：\u003ccode\u003e0000 1111\u003c/code\u003e）。任何 Hash 值与 \u003ccode\u003e15\u003c/code\u003e 进行 \u003ccode\u003e\u0026amp;\u003c/code\u003e 运算，高位都会被无情“抹零”，只有最后 4 位被保留下来：\u003c/p\u003e","title":"H mod C = H \u0026 (C - 1) ?"},{"content":"先撕掉一个谎言 你被灌输过一个观念——资源是稀缺的,所以竞争是残酷的,所以你穷是正常的。\n这句话每一个字都是错的。\n真正的稀缺,几乎从来不是物质的稀缺,而是认知的稀缺。石油曾经是让农田绝收的黑色毒水,稀土曾经是矿渣里没人要的废石,页岩气曾经被写进教科书当作\u0026quot;永远无法开采\u0026quot;的典型。它们不是突然变多了——它们一直在那里,只是人类的技术终于配得上它们了。\n所以请记住这个地基,因为后面所有的推理都建立在它之上:\n地球的资源上限是未知的,人类的需求是无限的。真正把你困住的,从来不是这个世界没有位置,而是你不知道位置在哪。\n明白了这一点,我们才能谈真正的问题——不是资源不够,而是人太懒,并且懒得如此整齐划一。\n一、\u0026ldquo;懒\u0026quot;是人性的底层代码,但懒有段位之分 先说清楚:我说的\u0026quot;懒\u0026rdquo;,不是道德批判。\n懒是人性的默认设置——用最小的付出,换取自己想要的生活。 这没有错,它甚至是效率的本能。真正决定你命运的,不是你懒不懒(你一定懒),而是你懒在哪个段位上。\n段位不同,议价能力天差地别。有人懒得一贫如洗,有人懒得财富自由。下面这五层,就是从地狱到天堂的完整阶梯。你现在站在第几层,决定了你现在过什么日子。\nflowchart TD A[\"LV1 懒到活不下去饥饿倒逼求生——不稳定,会被自动弹走\"] --\u003e B[\"LV2 懒到刚好活着被随意替换的螺丝钉——90%的人被困死在这里\"] B --\u003e C[\"LV3 拒绝被剥削看清了机制,愤怒觉醒——但光愤怒没用\"] C --\u003e D[\"LV4 转向勤奋逃离红海,构筑不可替代性——真正的分水岭\"] D --\u003e E[\"LV5 明知的勤奋哪里空缺补哪里——从心所欲不逾矩\"] style A fill:#3a1c1c,stroke:#e57373,color:#fff style B fill:#3a2e1c,stroke:#ffb74d,color:#fff style C fill:#3a3a1c,stroke:#fff176,color:#fff style D fill:#1c3a24,stroke:#81c784,color:#fff style E fill:#1c2e3a,stroke:#64b5f6,color:#fff 二、五层地狱与天堂 LV1｜懒到活着都成问题——这一层留不住人 理论上存在,现实中几乎没人能停在这里。\n原因很简单:再懒的人也要吃饭睡觉,这是生理的铁律。 当你懒到把自己饿倒的地步,饥饿会立刻接管你的身体,把\u0026quot;逃离饥饿\u0026quot;瞬间转化为\u0026quot;活下去\u0026quot;的暴力驱动。\n所以这一层是不稳定的——它自带一个弹射装置,会强行把人推向 LV2。你不需要努力离开它,饥饿会替你完成这件事。\n这一层的教训是:纯粹的懒,连活着都做不到。人被迫劳动的第一推动力,是恐惧。\nLV2｜懒到刚好能活着——90%的人死在这里,而且死得心安理得 这是绝大多数人的段位,也是最危险的段位——因为它足够舒适,舒适到让你放弃挣扎。\n刚好能活着,意味着你满足于温饱线上的生活:有口饭吃,有张床睡,还能刷刷手机。你不愿多付出一分,因为\u0026quot;活着\u0026quot;这个目标已经达成了。\n但你有没有想过,你为此付出的代价是什么?\n代价是:你的劳动彻底丧失了议价能力。\n想想供需关系。当你只求\u0026quot;活着\u0026quot;,你就等于向市场宣告——我可以被任何一个同样只求活着的人替换。而这样的人,遍地都是。你不是稀缺品,你是标准件。资本给标准件定价,从来不看你付出了多少,只看换掉你要花多少成本。而换掉你,几乎零成本。\n这才是剥削的真相:剥削不是资本太贪婪,而是你太普通。 你和身边所有人懒在同一条水平线上,你们互为替代品,于是你们集体丧失了定价权。资本只是顺水推舟,开出一个和你真实价值南辕北辙的工资——你还得感恩戴德。\n记住这句话:你穷,不是因为你不努力。恰恰是因为你和所有人一样努力。\nLV3｜懒到绝不承受剥削——觉醒的那一刻,愤怒是有价值的 某个瞬间,你突然看穿了这一切。\n你意识到:问题的根源不在于我不够卖力,而在于我和所有人长得一模一样。 正因为大家都懒在同一层,资本才敢肆无忌惮地压价。工资和劳动价值的背离,不是资本坏,是这个位置上的人可以随便换。\n这是觉醒。这份愤怒是宝贵的,它是你打破 LV2 舒适麻醉的唯一炸药。\n但请注意——光有愤怒,你依然一无所有。\n很多人卡死在这一层。他们看清了机制,于是终日抱怨资本、痛骂内卷、控诉不公。这些话全都对,但抱怨改变不了供需关系。你骂得再响,你依然是那个可替换的标准件。\nLV3的意识必须转化为LV4的行动,否则觉醒只会变成新的痛苦。 看清牢笼却走不出去,比看不清牢笼更折磨人。\nLV4｜转向勤奋——分水岭在此,答案只有四个字 这是整座阶梯的转折点,也是穷人和富人真正的分界线。\n觉醒之后,你必须想通一个残酷而简单的道理:\n要让你的劳动价值最大化、让收益百分之百归你自己,唯一的办法,就是让自己变得无法替代。\n而通往不可替代的路径,反直觉地,是远离人群。\n绝大多数人的本能是\u0026quot;哪里热闹去哪里\u0026quot;——看到手机赚钱,一万个人涌进去做手机;看到直播赚钱,一百万人挤进去做直播。这叫红海,红的是血。你在人山人海里拼命,拼到最后,你依然是标准件,只是换了个更拥挤的地方当标准件。\n真正的高手反其道而行:\n所有人都在做廉价手机?我去做高端手机。 所有人都在做高端沙发?我去做平价沙发。 所有人都在写同一种代码?我去写没人愿意碰的那种。\n为什么这一定行得通?因为第一节的地基在这里发挥威力了——人类需求无限,资源上限未知,这意味着永远存在没被填满的空地。 你找到那片空地,站上去,你就是那里唯一的供给。没有替代品,议价权就回到了你手里。工资不再由资本施舍,而由你定义。\n这就是勤奋的真正含义:勤奋不是比别人更卖力地挤同一扇门,而是聪明地去找那扇没人排队的门。\nLV5｜勤奋到主动\u0026quot;懒惰\u0026quot;——从心所欲不逾矩 当越来越多的人爬到 LV4,一场静默的革命就发生了。\n因为每个人都在死守自己不可替代的天地,红海继续饱和,而蓝海——那些没人填的空地——开始喷涌出惊人的财富。\n给你看一个必然发生的场景:\n当全世界的聪明人都跑去做沙发、做手机、写代码,总有一天,人们会猛然发现——没人照顾宠物了。 于是宠物照料者的收入飙升到离谱的高度,哪怕这份活儿本身轻松得可笑。\n这时,一部分 LV4 的高手会主动退下来,转身去当宠物照料者。\n请务必看懂这里的精妙之处:这不是懒惰的倒退,而是勤奋的最高形态。 他们不是干不动了才去干轻松活,而是清醒地判断出——市场的空缺在那里,财富的洼地在那里,于是从容地过去填补。\n他们的心态是:\n\u0026ldquo;我永远坚持勤奋的原则。但哪里出现空缺、哪里需要我,我就去哪里。表面上我切换到了更轻松的工作,本质上我是在响应市场最真实的呼唤。\u0026rdquo;\n这就是孔子说的\u0026quot;从心所欲不逾矩\u0026quot;的经济学版本——看似最懒,实则最勤;看似随性,实则精准。\n三、终局:市场自己长出来的共同富裕 把五层连起来看,你会发现人类正沿着一条清晰的轨道进化:\n盲目的懒 → 被剥削的痛 → 觉醒的怒 → 主动的勤 → 明知的勤。\n而驱动这台进化机器永不停转的燃料,自始至终只有一样——对剥削的意识。\n这一点必须说透:剥削不是进化的障碍,剥削是进化的引擎。 正是被剥削的切肤之痛,把人从 LV2 的舒适麻醉里痛醒,逼着他一层层往上爬。没有剥削,就没有觉醒;没有觉醒,就没有向上的动力。\n当足够多的人走完这条路,社会会呈现出一幅任何计划都设计不出的图景:\n每个人都占据着自己不可替代的天地,没人再是可以随意压价的标准件; 财富因此自发趋于均衡,不是被谁强行分配,而是因为红海无利可图,人们自然向蓝海分流; 哪里出现空缺,总有人从容补位,市场的每一个角落都被高价值的供给填满; 每个人进退有度,遵循的不是命令,而是市场最真实的需要。 这不是靠打土豪、分田地砸出来的平均。这是无数个体在拼命追求自身价值最大化的过程中,像潮水一样自发形成的共同富裕。\n它的终极姿态,浓缩成一句话:\n\u0026ldquo;工作切换,我可以懒下来;但只要哪里需要我,我随时提刀上马。\u0026rdquo;\n而这一切的起点,不过是有一天,你终于受够了当标准件,决定去找一片只属于你的天地。\n作者 洛克里亚的五度 ","permalink":"https://lv-blog.pages.dev/posts/thoughts/%E6%87%92%E7%9A%84%E5%A2%83%E7%95%8C/","summary":"\u003ch2 id=\"先撕掉一个谎言\"\u003e先撕掉一个谎言\u003c/h2\u003e\n\u003cp\u003e你被灌输过一个观念——\u003cstrong\u003e资源是稀缺的,所以竞争是残酷的,所以你穷是正常的。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e这句话每一个字都是错的。\u003c/p\u003e\n\u003cp\u003e真正的稀缺,几乎从来不是物质的稀缺,而是\u003cstrong\u003e认知的稀缺\u003c/strong\u003e。石油曾经是让农田绝收的黑色毒水,稀土曾经是矿渣里没人要的废石,页岩气曾经被写进教科书当作\u0026quot;永远无法开采\u0026quot;的典型。它们不是突然变多了——它们一直在那里,只是人类的技术终于配得上它们了。\u003c/p\u003e\n\u003cp\u003e所以请记住这个地基,因为后面所有的推理都建立在它之上:\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e地球的资源上限是未知的,人类的需求是无限的。真正把你困住的,从来不是这个世界没有位置,而是你不知道位置在哪。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e明白了这一点,我们才能谈真正的问题——不是资源不够,而是\u003cstrong\u003e人太懒,并且懒得如此整齐划一\u003c/strong\u003e。\u003c/p\u003e\n\u003ch2 id=\"一懒是人性的底层代码但懒有段位之分\"\u003e一、\u0026ldquo;懒\u0026quot;是人性的底层代码,但懒有段位之分\u003c/h2\u003e\n\u003cp\u003e先说清楚:我说的\u0026quot;懒\u0026rdquo;,不是道德批判。\u003c/p\u003e\n\u003cp\u003e懒是人性的默认设置——\u003cstrong\u003e用最小的付出,换取自己想要的生活。\u003c/strong\u003e 这没有错,它甚至是效率的本能。真正决定你命运的,不是你懒不懒(你一定懒),而是\u003cstrong\u003e你懒在哪个段位上。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e段位不同,议价能力天差地别。有人懒得一贫如洗,有人懒得财富自由。下面这五层,就是从地狱到天堂的完整阶梯。你现在站在第几层,决定了你现在过什么日子。\u003c/p\u003e\n\u003cpre class=\"mermaid\"\u003eflowchart TD\n    A[\"LV1 懒到活不下去\u003cbr/\u003e饥饿倒逼求生\u003cbr/\u003e——不稳定,会被自动弹走\"] --\u003e B[\"LV2 懒到刚好活着\u003cbr/\u003e被随意替换的螺丝钉\u003cbr/\u003e——90%的人被困死在这里\"]\n    B --\u003e C[\"LV3 拒绝被剥削\u003cbr/\u003e看清了机制,愤怒觉醒\u003cbr/\u003e——但光愤怒没用\"]\n    C --\u003e D[\"LV4 转向勤奋\u003cbr/\u003e逃离红海,构筑不可替代性\u003cbr/\u003e——真正的分水岭\"]\n    D --\u003e E[\"LV5 明知的勤奋\u003cbr/\u003e哪里空缺补哪里\u003cbr/\u003e——从心所欲不逾矩\"]\n\n    style A fill:#3a1c1c,stroke:#e57373,color:#fff\n    style B fill:#3a2e1c,stroke:#ffb74d,color:#fff\n    style C fill:#3a3a1c,stroke:#fff176,color:#fff\n    style D fill:#1c3a24,stroke:#81c784,color:#fff\n    style E fill:#1c2e3a,stroke:#64b5f6,color:#fff\n\u003c/pre\u003e\n\u003ch2 id=\"二五层地狱与天堂\"\u003e二、五层地狱与天堂\u003c/h2\u003e\n\u003ch3 id=\"lv1懒到活着都成问题这一层留不住人\"\u003eLV1｜懒到活着都成问题——这一层留不住人\u003c/h3\u003e\n\u003cp\u003e理论上存在,现实中几乎没人能停在这里。\u003c/p\u003e\n\u003cp\u003e原因很简单:\u003cstrong\u003e再懒的人也要吃饭睡觉,这是生理的铁律。\u003c/strong\u003e 当你懒到把自己饿倒的地步,饥饿会立刻接管你的身体,把\u0026quot;逃离饥饿\u0026quot;瞬间转化为\u0026quot;活下去\u0026quot;的暴力驱动。\u003c/p\u003e\n\u003cp\u003e所以这一层是不稳定的——它自带一个弹射装置,会强行把人推向 LV2。你不需要努力离开它,饥饿会替你完成这件事。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e这一层的教训是:纯粹的懒,连活着都做不到。人被迫劳动的第一推动力,是恐惧。\u003c/strong\u003e\u003c/p\u003e\n\u003ch3 id=\"lv2懒到刚好能活着90的人死在这里而且死得心安理得\"\u003eLV2｜懒到刚好能活着——90%的人死在这里,而且死得心安理得\u003c/h3\u003e\n\u003cp\u003e这是绝大多数人的段位,也是\u003cstrong\u003e最危险的段位\u003c/strong\u003e——因为它足够舒适,舒适到让你放弃挣扎。\u003c/p\u003e\n\u003cp\u003e刚好能活着,意味着你满足于温饱线上的生活:有口饭吃,有张床睡,还能刷刷手机。你不愿多付出一分,因为\u0026quot;活着\u0026quot;这个目标已经达成了。\u003c/p\u003e\n\u003cp\u003e但你有没有想过,你为此付出的代价是什么?\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e代价是:你的劳动彻底丧失了议价能力。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e想想供需关系。当你只求\u0026quot;活着\u0026quot;,你就等于向市场宣告——我可以被任何一个同样只求活着的人替换。而这样的人,遍地都是。你不是稀缺品,你是标准件。资本给标准件定价,从来不看你付出了多少,只看\u003cstrong\u003e换掉你要花多少成本\u003c/strong\u003e。而换掉你,几乎零成本。\u003c/p\u003e\n\u003cp\u003e这才是剥削的真相:\u003cstrong\u003e剥削不是资本太贪婪,而是你太普通。\u003c/strong\u003e 你和身边所有人懒在同一条水平线上,你们互为替代品,于是你们集体丧失了定价权。资本只是顺水推舟,开出一个和你真实价值南辕北辙的工资——你还得感恩戴德。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e记住这句话:你穷,不是因为你不努力。恰恰是因为你和所有人一样努力。\u003c/strong\u003e\u003c/p\u003e\n\u003ch3 id=\"lv3懒到绝不承受剥削觉醒的那一刻愤怒是有价值的\"\u003eLV3｜懒到绝不承受剥削——觉醒的那一刻,愤怒是有价值的\u003c/h3\u003e\n\u003cp\u003e某个瞬间,你突然看穿了这一切。\u003c/p\u003e\n\u003cp\u003e你意识到:\u003cstrong\u003e问题的根源不在于我不够卖力,而在于我和所有人长得一模一样。\u003c/strong\u003e 正因为大家都懒在同一层,资本才敢肆无忌惮地压价。工资和劳动价值的背离,不是资本坏,是这个位置上的人可以随便换。\u003c/p\u003e\n\u003cp\u003e这是觉醒。这份愤怒是宝贵的,它是你打破 LV2 舒适麻醉的唯一炸药。\u003c/p\u003e\n\u003cp\u003e但请注意——\u003cstrong\u003e光有愤怒,你依然一无所有。\u003c/strong\u003e\u003c/p\u003e","title":"懒惰的五个层级：你穷,可能不是因为你不努力,而是因为你和所有人一样“努力”"},{"content":"主流算法 固定窗口计数器 每秒一个计数器，+1超过阈值就拒绝\n缺点 0.51s 和 1.51s 之间产生2倍流量\n滑动窗口 把时间切成更细的小格，滑动统计\n缺点 解决临界问题，但是实现复杂\n漏桶 请求进桶，匀速流出，桶满则弃\n缺点流出速度恒定，扛不了正常的突发\n令牌桶 匀速往桶里放令牌，来请求拿令牌，没令牌被拒绝\n缺点允许一定突发\n","permalink":"https://lv-blog.pages.dev/posts/programming/backend/%E9%99%90%E6%B5%81%E9%99%8D%E7%BA%A7%E7%A0%94%E7%A9%B6%E7%AC%94%E8%AE%B0/","summary":"\u003ch2 id=\"主流算法\"\u003e主流算法\u003c/h2\u003e\n\u003ch3 id=\"固定窗口计数器\"\u003e固定窗口计数器\u003c/h3\u003e\n\u003cp\u003e每秒一个计数器，+1超过阈值就拒绝\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e缺点\u003c/strong\u003e 0.51s 和 1.51s 之间产生2倍流量\u003c/p\u003e\n\u003ch3 id=\"滑动窗口\"\u003e滑动窗口\u003c/h3\u003e\n\u003cp\u003e把时间切成更细的小格，滑动统计\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e缺点\u003c/strong\u003e 解决临界问题，但是实现复杂\u003c/p\u003e\n\u003ch3 id=\"漏桶\"\u003e漏桶\u003c/h3\u003e\n\u003cp\u003e请求进桶，匀速流出，桶满则弃\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e缺点\u003c/strong\u003e流出速度恒定，扛不了正常的突发\u003c/p\u003e\n\u003ch3 id=\"令牌桶\"\u003e令牌桶\u003c/h3\u003e\n\u003cp\u003e匀速往桶里放令牌，来请求拿令牌，没令牌被拒绝\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e缺点\u003c/strong\u003e允许一定突发\u003c/p\u003e","title":"限流降级研究笔记"},{"content":"1. 屏幕高度必须与视线平齐 屏幕上缘应接近或略低于眼睛水平线，禁止长期低头使用设备。\n2. 上肢支撑优先于肩部用力 键盘与鼠标位置需确保手肘自然下垂约90度，避免肩部抬起或前伸代偿。\n3. 头部位置以“耳朵对肩”为标准 耳朵应位于肩峰正上方或略后方，一旦出现前伸即为错误姿势。\n4. 静态坐姿持续时间不得超过45分钟 每30–45分钟必须进行至少30秒站立或活动调整，打断颈部静态负荷。\n5. 禁止长时间塌陷式坐姿 避免骨盆后倾、胸椎塌陷及“窝在椅子里”使用电脑的姿势。\n6. 每日进行基础颈深屈肌激活训练 执行下巴后收（chin tuck）10次，用于恢复头颈中立位控制能力。\n核心原则 保持头颈中立位、减少前伸负荷、避免长时间静态固定。\n","permalink":"https://lv-blog.pages.dev/posts/life/reduce-neck-pain-from-prolonged-computer-use/","summary":"\u003ch2 id=\"1-屏幕高度必须与视线平齐\"\u003e1. 屏幕高度必须与视线平齐\u003c/h2\u003e\n\u003cp\u003e屏幕上缘应接近或略低于眼睛水平线，禁止长期低头使用设备。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"2-上肢支撑优先于肩部用力\"\u003e2. 上肢支撑优先于肩部用力\u003c/h2\u003e\n\u003cp\u003e键盘与鼠标位置需确保手肘自然下垂约90度，避免肩部抬起或前伸代偿。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"3-头部位置以耳朵对肩为标准\"\u003e3. 头部位置以“耳朵对肩”为标准\u003c/h2\u003e\n\u003cp\u003e耳朵应位于肩峰正上方或略后方，一旦出现前伸即为错误姿势。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"4-静态坐姿持续时间不得超过45分钟\"\u003e4. 静态坐姿持续时间不得超过45分钟\u003c/h2\u003e\n\u003cp\u003e每30–45分钟必须进行至少30秒站立或活动调整，打断颈部静态负荷。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"5-禁止长时间塌陷式坐姿\"\u003e5. 禁止长时间塌陷式坐姿\u003c/h2\u003e\n\u003cp\u003e避免骨盆后倾、胸椎塌陷及“窝在椅子里”使用电脑的姿势。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"6-每日进行基础颈深屈肌激活训练\"\u003e6. 每日进行基础颈深屈肌激活训练\u003c/h2\u003e\n\u003cp\u003e执行下巴后收（chin tuck）10次，用于恢复头颈中立位控制能力。\u003c/p\u003e\n\u003chr\u003e\n\u003ch1 id=\"核心原则\"\u003e核心原则\u003c/h1\u003e\n\u003cp\u003e保持头颈中立位、减少前伸负荷、避免长时间静态固定。\u003c/p\u003e","title":"久坐用电脑如何最大程度减少脖子酸痛"},{"content":"什么是一般连拍和速度优先连拍？ 在［速度优先连拍］模式下，当半按下快门按钮时，为第一张影像决定对焦，并且后续拍摄的对焦被锁定。\n","permalink":"https://lv-blog.pages.dev/posts/photography/some-knowledge-about-cameras/","summary":"\u003ch2 id=\"什么是一般连拍和速度优先连拍\"\u003e什么是一般连拍和速度优先连拍？\u003c/h2\u003e\n\u003cp\u003e在［速度优先连拍］模式下，当半按下快门按钮时，为第一张影像决定对焦，并且后续拍摄的对焦被锁定。\u003c/p\u003e","title":"个人总结的一些相机小知识"},{"content":"\n","permalink":"https://lv-blog.pages.dev/posts/essay/my-memes/","summary":"\u003cp\u003e\u003cimg alt=\"e7f25431a18bc666efd80900_0\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/e8c4861be2a16edf383f8ae2fcdd9384198986194f48f074fcb61e977c063bea.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"xiao\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/fc36eeff6336a60beb8f84ed853f3c7b0c43dc7161ccd0f4037af29453a54684.jpg\"\u003e\u003c/p\u003e","title":"自创memes"},{"content":"\n","permalink":"https://lv-blog.pages.dev/posts/photography/my-high-school/","summary":"\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/bbbfd6f639993853226ca77dce4cef974778adfeed921fbf0715dc51e0d8fec8.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/06190161495031d8b46e55489a3af31d91a37b0cef96f3a58ec08c79f89fe0d5.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/e2bb531c0a7e05f35817767c804226f5cac1402678b3357ca984913e60681dda.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/4974b5b413441c35874e904b6fabf00c3da42b58081e9e361c0ece4c2858c2df.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/d1bf87f1797a2ff1f910467f7b4b151fc70f9e20e394485870ee30f55d6121cc.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/7efe09d48dc3bb7d95d1630b3b3f94fbda346a457d9d7bd7b1f55e5bc9bf5170.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/a372e5aea2f9a77a36dfe4d86bed0f26510d67fc835131a3230de2cf2b5c389e.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/1c1359ad14d0aa0a04922bbfd556df1e2067b9335e947cecbd56501687a3f15b.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/5653ba038822de550f5c9e62efc7c7913b958c69ba4da7a3447c2bed85087465.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/f50126718a1b499eb6d0ef7e83cadc33b46571bdc8bdeb8ef7eb607bbe4b85b9.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/341ae5abc98e5d041ba380b7921d859d29fe5aebbdcea5838edb2f1174ea39c7.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/a5da724b5eba999d042f672a7ee6b18e1f265a209f04158a3ad015f56f813e70.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/9ea1d10b3e37b789162a77e494464f40cd3c21135af65440e62352c51ec5f820.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/276ce9c1bf6c7597832013b0cf3da7b2158ef4d8c53548e57729145eb58e911a.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/094c042dd111df0fee94234add31687230017b7286358dfc1a362dc7f16d9124.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/6055a52e3aaf081506c4477379d943f84e6421d49d10bda5a30ba9a6392f925f.jpg\"\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/22483077cced17019f80385f6cfb8cfc847241e58e6004a3a4cf5bacb87f884a.jpg\"\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/90b850563e3249735d389eb24193de63e4440e32154e7862eafc5e86c1aec02f.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/f5e207a74abb25de53aa0b664ab813acc9f69446d52dfdf920c07c1f3d24e55f.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/8a497511087f9a2043cf5e3ffbe8569d34e1bc4492a45e4ab678c2ad809249c0.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/7c9ea6784a7d68745197ae2bc9acdbb5eb22944ae8194be350ca61dbb142da69.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/e62689cdd7e5d6bc48e3383aa1f60da5dac561b1ee41e1514fcaa9901110a9b6.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/b5f1eede3d6b31ae18a065901f6cca58a17dc3a2452ab19eb8f4a9e0cc34c596.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/e3c42f6410fcca63f7b1f5e841788348104b4dc78072911d4cbc7dddb45440d8.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/e1da3ba50a6b9539a3472886008f7c759f1470054a4ccfa9fd5f81b9362449b0.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/151b105e82ad9d901a3b05d4393234844f1d63f8d29f364c73c93754d84292f2.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/9443e4639f49e3304ea9d70d7aeb553e1eaeb26634006587bc559b2a64d5faea.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"以前的二中，现在的周恩来红军中学\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/8086c178c5aa01fe1fbc413c0b5f9a6345f9d51ecc03f84e684719270866762d.jpg\"\u003e\u003c/p\u003e\n\u003cstyle\u003e\n  img {\n    width: 100%;\n  }\n\n  .hover-alt-wrapper {\n    position: relative;\n    display: inline-block;\n    line-height: 0;\n    width: 100%;\n  }\n\n  .alt-tooltip {\n    position: absolute;\n    left: 50%;\n    bottom: 6px;\n\n    padding: 20px;\n\n    height: 24px;\n     \n\n    transform: translateX(-50%) translateY(4px);\n\n    background: rgba(0, 0, 0, 0.6);\n    color: #fff;\n    font-size: 12px;\n\n    display: flex;\n     \n    align-items: center;\n    justify-content: center;\n\n    border-radius: 4px;\n    pointer-events: none;\n\n    opacity: 0;\n    transition: 0.15s ease;\n\n    white-space: nowrap;\n     \n    overflow: hidden;\n     \n    text-overflow: ellipsis;\n     \n  }\n\n  .hover-alt-wrapper.show .alt-tooltip {\n    opacity: 1;\n    transform: translateX(-50%) translateY(0);\n  }\n\n  .img-open-btn {\n    position: absolute;\n    top: 6px;\n    right: 6px;\n\n\n    background: rgba(0, 0, 0, 0.5);\n    color: #fff;\n\n    border-radius: 4px;\n    text-align: center;\n\n    cursor: pointer;\n    user-select: none;\n\n    opacity: 0;\n    transition: 0.15s ease;\n\n    padding: 10px;\n  }\n\n  .hover-alt-wrapper:hover .img-open-btn {\n    opacity: 1;\n  }\n\n  .img-open-btn {\n    position: absolute;\n    top: 20px;\n    right: 20px;\n\n    background: rgba(0, 0, 0, 0.5);\n    color: #fff;\n\n    border-radius: 4px;\n    font-size: 14px;\n    line-height: 22px;\n    text-align: center;\n\n    cursor: pointer;\n    user-select: none;\n\n    opacity: 0;\n    transition: 0.15s ease;\n  }\n\u003c/style\u003e\n\n\u003cscript\u003e\n  document.querySelectorAll(\"img\").forEach(img =\u003e {\n    \n    const wrapper = document.createElement(\"div\");\n    wrapper.className = \"hover-alt-wrapper\";\n\n    \n    img.parentNode.insertBefore(wrapper, img);\n    wrapper.appendChild(img);\n\n    \n    const tooltip = document.createElement(\"div\");\n    tooltip.className = \"alt-tooltip\";\n    tooltip.textContent = img.alt || \"\";\n\n    wrapper.appendChild(tooltip);\n\n    const btn = document.createElement(\"div\");\n    btn.className = \"img-open-btn\";\n    btn.innerHTML = \"⤢\";\n\n\n    btn.addEventListener(\"click\", (e) =\u003e {\n      e.stopPropagation();\n      window.open(img.src, \"_blank\");\n    });\n\n    wrapper.appendChild(btn);\n\n\n    \n    wrapper.addEventListener(\"mouseenter\", () =\u003e {\n      if (img.alt) wrapper.classList.add(\"show\");\n    });\n\n    wrapper.addEventListener(\"mouseleave\", () =\u003e {\n      wrapper.classList.remove(\"show\");\n    });\n  });\n\u003c/script\u003e","title":"我的高中"},{"content":"\n","permalink":"https://lv-blog.pages.dev/posts/photography/my-old-works/","summary":"\u003cp\u003e\u003cimg alt=\"等的不过是某个人回来的电影\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/f2a36df9530e553ef5ae3117b4abc7facd247a67dc317a8a3e54fe9a8c47dfc8.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Enjoy the pain 苦中作乐\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/ee869d18f99c581bb719d14cd598c80030c5e83ab90505c252f3cb17aec7de95.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"我自己\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/e60a3f97714bda4a3573d2159973e00a0556555945f812d6867bf494ac41e68e.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"颐和园\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/4340135be6a56859b81fb38c9048a249b1d8ac081770da18e60980d063f0ad29.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"淮安某个桥\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/735d8d1034a29ae6c27c48f0b47e6e2f3c76ec3594125015c6cb652fb75873de.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"镇江晨曦\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/9fafdfcb680fdd11a90981eb6673c0e5e406f510711862590c2cb456f11bf87e.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"街头艺人\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/10c6457b1f7b454da3bc48ed6c0d46feaddd8aa84cf6dd1b64750a7ecdf8ce63.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"桥\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/22b915575e31d259febceb1b6b3d0f0c5e20b901067dbf808a1bb2c83ebb8d83.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"南山日落\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/328ec4ae443e1cecb5abce80036c69db70e6318c1cf414e2e1a035c9be8077b8.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"公园长椅\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/1a8e94b9ecdfdd130f3bbda3774190dd3954584dbd29e844992ad0c1a08ea74c.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"江滨\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/f9d1dcc1bc3ebcaf0af2555ba987abe8adfae87804266631c5fb50ca791d54fb.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"江苏大学附近摄\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/5c5891360e46cfe89b53587c41b9cd3fa42ca5d43351ae5eb03557e542e06a5a.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"1_2 1_4 ？\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/1a8f91cb5c46227ed6daf8163279c35bad2da6731896c8a9b48375e517c69ff3.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"6_32 5B408\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/898456929bcde67be27deb3de85b33068edbd39d0292e3d198ac823348f695ac.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"晚霞 86路\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/e03c4bb30d26924acf46a4a29f881c91173c326829fd89215dfbb740857cfbdb.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"琴键\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/9e2eb8d071792812832fd48e059efe2f3c804f640fe74ce2b74f0ef5fed3284c.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"七点食堂旁\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/875d07906713d7a0dfc2cd4683af8ddd6b790d4ffea3404b04457351c6f63b27.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"阴天\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/3b9a1b3cff82d292086a9a953d9d05eca9231b6bf6c32e2149dcf86812d1d180.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"晚上七点\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/a80c520b5b25a00fdedce0a72da3ccfec5aa02189993901fbfbd8165d0a2893c.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"屋顶\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/41741704348fcb018902b257236b09a3d2ac4f7f3eb3b4713b9da1a088f31f86.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"86年凤凰自行车\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/6b821ab2fc7b399361238af9e1fce279323523b539569069d24a9f2fc4ff84d4.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"大市口附近摄\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/dc696dd9ea6d57fb4b81b07d41401eb6291e3c4572ac5e819b6d8d36ee0b8567.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"吉他谱\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/80758f56b4ff03fd4aaf6ffcbf085e19e3808fd8ad21ee1e4bd9b5f8100252dd.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"吉他谱\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/fd509d55906655f80eadce9937932d8a99010b5f8feb19f4fbf704ba17b12a26.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Em7(no5)_D\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/45879702a69edd433509cde3e93da1c0e58113ce3151fd8db166d549704312a0.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"生活还是不够精彩，即便天再大\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/5258385c63b7bd8223252539e51a84cf6ec32def4c9b733e03a25d54271b6590.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"范特西天空\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/a7a59cadfc68eab73a29b8ab59d322222a2d6b41bb10cece4a3a8d46c0602da8.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"月亮，蛋糕\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/744a7e34825d92e4d703563fb72ae8667c662164b0edb69cc54b84b3b85339ed.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"本来就是个巧合，却又那么错落有致\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/fb57698997d8502a028f5ecd4ff352d0d90e4934c0b71e99f7efff966a357512.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"我自己\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/705c7f583d2ca06a8d8e554b7e6a7bf18200badf7c00b9c59472e5a2e4cc684d.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"昨天傍晚在男生宿舍楼后面拍到的\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/74e2b1ece4742cc745006fc67731f9e35302423ae1c5840141d13bad9fd6c6b4.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"生活过于正常，内心毫无波澜，就像这堵除了乳胶漆以外一无所有的墙\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/e223e384e174c1583ce5a6fffbd8383977f9d444143e2e95e0423088dcdcd63f.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"一抹红\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/80b25291fd7a9cc297c8654aa8d4498bd03a08777a869262ab880d55fce43949.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"雪下画雪\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/2618a659138239a9ca5f5b30f2c50ad1743ee81ae4442999055dc301fbf6bd69.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"后街後街\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/9e88e35b3bbecefeadefebf35715c9b4806efbb32874ca0202e0fea2779ac39e.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"Wathan\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/c322a18d4decca55edf599660ebcbe1adec6c17be8ca20cd08a0748db5c6fdd1.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"我女朋友会写字了\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/2ed5e27843f0689b903d113850be110ea228e543644c7ef0dc6ce7988c63e46f.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"因为镇江旱情\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/43b2cf67fcc947d6a5ab4f85c5df7a0e8830158656f8f84e493f1de43a141687.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"失信的教官\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/36f939e681a6c54a208ac49865986c29a77f33e9f058b64951d60e285f8eec06.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"孤独的车\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/3f2d3c68f94cd86a2eca644aa358967f7db5b7ea21b5f9ffa261eb673b540b4b.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"弦上染了铜绿\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/7792607fe1b3998dc5e5331b4851d6c9028a1fa9d0b887e589623743b150ae35.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"我在新校区体育馆旁边拍到的 飞机，蓝天，围墙，如此简单。\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/4976e67d43df374110c880e23852f794bdcf7321fb6adbad460dd1f36d904a01.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"我捕捉了教室桌子上掠过的一道[e401328]\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/675b54a30f61955bac4aca5311b879f6d46a7b22317a9323f80636712ca2a5ae.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"梦溪广场西站\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/3c04d757756ab1bf8adeed4492e203c78b71b7daa05721eaf2b72a00811c2ec0.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"每天用来写的东西，当然也要有自己的自拍。\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/ea9741b6fe6ed1c926bc6216aa03936dd72b75bc7225e8d46cb56f297edd3a8a.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"这车窗有点sweet\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/375518560d8ddaea8d9592d5349d262e109d28afd63d6ec672292ffc99b6540a.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"斑驳\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/01a3c95c4b59329a4ff40be344e84257216addd7c3b4f0eb4290944e0cba5de0.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"列车\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/842b3d147c019026145abcb9bc45167296c697d55fe99925dfd6de75a4d229fb.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"窗帘\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/795a7347123524946e7ef39f531bff0f29d30890ef87705b41bdbb07007fcb4a.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"教室\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/c8e25d4c8f3abc051831d385e9707293627e90272e125734b1870849f378ea56.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"昨天下午6点50\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/8b10460bc91167b310be51c17db4a3d45ce54884fefcb299e0e1417ad9d23e98.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"再过一个月就看不到这样的风景了\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/b64546750755b90fbab12b546c020a498a13fb7483244d58a902e9568154e1d6.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"万丈光芒 西长街\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/fd066261e3260f0505a5d241acf7403d2cf9455f8c03324f1f7ea5d6478fe91f.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"阴影\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/b041681a622965121bbd39955dee6684f6a990f2fb8f443926f9bf09937744bb.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"绿\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/5beb8b6b7c9921b53990245d443f88f8fb08818c34d10e4a22ce657954937d1f.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"天空\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/b5f44e2b24d9e3c78aac80ec25e2f7e711c46eb698e750419c041c27b47a2985.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"音乐节\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/a641d0ca1030e776f7486a45b1fd4eb01152da5a6fd039f8f43aed4eb3d275a8.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"一直准专业\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/2d18eed4608b96d3e7b37b0c9a82bdd88a743dcbca7c6f46b4e05168bd8058f1.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"多嘴麻雀 揚州東關街\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/59425b4599f1de4a81434d0cb6615a0f8bdd2ebf7add75a15bf92088992cdbc1.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"能有哪个风景最后不留给世俗\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/e8df91c3798ea190377a3863c1b17ad955b2b0aaa6edd350fc0ab2db38fd348e.jpg\"\u003e\u003c/p\u003e","title":"以前的摄影作品"},{"content":"🎻 古典音乐（Classical / Neo-classical / Academic） 有严肃作曲结构（主题发展、变奏、奏鸣曲式等） （包含严格古典作品 + 现代古典 / 新古典） 序号 艺术家 / 作曲家 曲名 1 马克西姆 克罗地亚狂想曲 2 坂本龙一 Energy Flow 3 坂本龙一 Merry Christmas Mr. Lawrence 4 Mason Ma Summer Night Waltzes Op.12 No.3 5 何占豪 / 陈钢 梁山伯与祝英台 致爱丽丝（原版）\n🎧 非古典音乐（流行 / 影视 / 游戏 / 跨界 / ……） 序号 艺术家 / 作曲家 曲名 1 Rabpit Dream 2 周杰伦 路小雨 3 周杰伦 早操 ","permalink":"https://lv-blog.pages.dev/posts/piano/my-piano-pieces/","summary":"\u003ch2 id=\"-古典音乐classical--neo-classical--academic\"\u003e🎻 古典音乐（Classical / Neo-classical / Academic）\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e有严肃作曲结构（主题发展、变奏、奏鸣曲式等）\n（包含严格古典作品 + 现代古典 / 新古典）\u003c/li\u003e\n\u003c/ul\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e序号\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e艺术家 / 作曲家\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e曲名\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e马克西姆\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e克罗地亚狂想曲\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e坂本龙一\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eEnergy Flow\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e3\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e坂本龙一\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMerry Christmas Mr. Lawrence\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e4\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMason Ma\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSummer Night Waltzes Op.12 No.3\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e何占豪 / 陈钢\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e梁山伯与祝英台\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e致爱丽丝（原版）\u003c/p\u003e\n\u003ch2 id=\"-非古典音乐流行--影视--游戏--跨界--\"\u003e🎧 非古典音乐（流行 / 影视 / 游戏 / 跨界 / ……）\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e序号\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e艺术家 / 作曲家\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e曲名\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eRabpit\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDream\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e周杰伦\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e路小雨\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e3\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e周杰伦\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e早操\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e","title":"我会弹奏的钢琴曲"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/life/foods-i-like/","summary":"\u003cstyle\u003e\nimg {\n  width:60%\n}\n\u003c/style\u003e\n\u003cp\u003e\u003cimg alt=\"image-20260616185540839\" loading=\"lazy\" src=\"https://img.wathan.cn/20260616185541263.webp\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20260616185600384\" loading=\"lazy\" src=\"https://img.wathan.cn/20260616185600630.webp\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20260616185618930\" loading=\"lazy\" src=\"https://img.wathan.cn/20260616185619523.webp\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20260616185626382\" loading=\"lazy\" src=\"https://img.wathan.cn/20260616185626539.webp\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20260616185634325\" loading=\"lazy\" src=\"https://img.wathan.cn/20260616185634488.webp\"\u003e\u003c/p\u003e\n\u003cp\u003e\u003cimg alt=\"image-20260616185643787\" loading=\"lazy\" src=\"https://img.wathan.cn/20260616185644009.webp\"\u003e\u003c/p\u003e","title":"我喜欢的好吃的"},{"content":"Before : Means When I was A little BOY Now : just , Now ==============[now,I will tell everyone in my QQ list who I am,and what I like]\nBefore;Favourite Thing I wanna Do : Play Computer Game(Espeicially Red;Counter Stike),Play Toy Block(Dream about everything) Now;Favourite Thing I wanna Do : Write The Programe(Of course,and Drawing , playing GUITAR)\nBefore;The Thing I still Remember and unforgetable : Reported The teacher Divine The Class , after then , She gave me a slap on face twice Now;The Thing I still Remember and unforgetable:*********(Can\u0026rsquo;t Tell U)\nBefore;My favourite Gender:Girl/Woman(Like playing with GIRLS) Now;My favourite Gender:None(I\u0026rsquo;m not LOVE with myself)\nBefore;Person ability : VERY OUT-GOING Now;Person ability : VERY SHY\nBefore;State of business : POOR now;State of business : NOT itch , NOT poor\nBefore;Favourite kind of movies : 3D Cartoons , The science Fication Film (Very Childish) , such as 快乐星球, Wall-E now;Favourite kind of movies : The science Fication Film (Realistic) , Fables , Horror Film , the film what very PH\nBefore;The crazy Thing : Compuer Games . now;The crazy Thing : Write The Computer Programe\nBefore;The Marks ability : Low now;The Marks ability : Low still\nBefore;Achievements : None (Had Much Computer Game Experience , Is it ?) now;Achievements :Can Create Sth by Using BIG SOFT .\nBefore;The most Reget Thing I did : now;\nBefore; now; 小时候最喜欢做的事情：玩电脑游戏（特别是红色警戒、反恐精英）、搭积木（天马行空的想象）\n现在最喜欢做的事情：编程（当然乐器和素描也都在内啦）\n小时候印象最深刻的事情：举报老师占课被在全班同学面前赏了两巴掌\n到现在印象最深刻的事情：********（这件事情事关个人隐私，恕不透露）\n小时候最喜欢的性别：女（喜欢和女生一起玩）\n现在最喜欢的性别：既不是同性恋，又对恋爱提不上兴趣，属于无性恋者（恕我者CPU男女互不兼容）\n小时候的性格：外向\n现在的性格：内向\n小时候的家庭经济状况：很糟，我只有和我的狗取乐\n现在的家庭经济状况：二流家庭，不富有也不穷，这很好\n小时候最喜欢看的电影类型：3D动画、完全不符合逻辑的科幻片（如快乐星球、机器人瓦力）\n现在最喜欢看的电影类型：很靠谱不是很夸大现实的科幻片、成人童话、恐怖片、富有哲理的片子\n小时候最痴迷的东西：电脑游戏\n现在最痴迷的东西：恕我会回答是编程\n小时候的成绩：很糟糕\n现在的成绩：依旧很糟糕\n小时候的成就：无（玩过很多游戏？有很多游戏经验？）\n现在的成就：学会使用大型计算机编辑制作软件创作一些东西 小时候干过的最后悔的事：曾拒绝与一位我现在认为很好的女生交往（小时候不懂事）\n现在觉得干过的最后悔的事：小时候没学什么有价值的技能 小时候最认为幸福的事：（这需要详细描写）↓\n一台笔记本电脑，其中有一款叫做魔兽争霸的游戏，笔记本电脑旁有这几样东西：方便面、恰恰瓜子、可口可乐。笔记本电脑右方，有一台非液晶电视机，正在播放名侦探柯南。整个家就我一个人，父母都出去了。没有任何学校的作业负担等。我玩着游戏，游戏在laoding时看会儿电视。玩够了吃一点方便面，然后喝一点饮料。游戏结束了，赶紧拿出瓜子吃，看名侦探柯南。\n现在认为最幸福的事： 唉。没有。 …………\n","permalink":"https://lv-blog.pages.dev/posts/essay/something-i-wrote-when-i-was-a-kid/","summary":"\u003cp\u003eBefore : Means When I was A little BOY\nNow : just , Now\n==============[now,I will tell everyone in my QQ list who I am,and what I like]\u003c/p\u003e\n\u003cp\u003eBefore;Favourite Thing I wanna Do : Play Computer Game(Espeicially Red;Counter Stike),Play Toy Block(Dream about everything)\nNow;Favourite Thing I wanna Do : Write The Programe(Of course,and Drawing , playing GUITAR)\u003c/p\u003e\n\u003cp\u003eBefore;The Thing I still Remember and unforgetable : Reported The teacher Divine The Class , after then , She gave me a slap on face twice\nNow;The Thing I still Remember and unforgetable:*********(Can\u0026rsquo;t Tell U)\u003c/p\u003e","title":"小学的时候在电脑上写的"},{"content":"越是相同，越应该结合，因为彼此更容易理解对方的性格； 但是越是相同，也越不应该结合，因为在冷场时，我们同时选择不接收对方的音讯。\n越是不同，越应该结合，因为优势互补，形成完美的答案； 但是越是不同，也越不应该结合，因为我们很少有时间，握着手开心地听同一场演唱会。\n","permalink":"https://lv-blog.pages.dev/posts/essay/together-or-not/","summary":"\u003cp\u003e越是相同，越应该结合，因为彼此更容易理解对方的性格；\n但是越是相同，也越不应该结合，因为在冷场时，我们同时选择不接收对方的音讯。\u003c/p\u003e\n\u003cp\u003e越是不同，越应该结合，因为优势互补，形成完美的答案；\n但是越是不同，也越不应该结合，因为我们很少有时间，握着手开心地听同一场演唱会。\u003c/p\u003e","title":"Together or Not"},{"content":" 身高 179cm ","permalink":"https://lv-blog.pages.dev/posts/life/daily/everyday-weight/","summary":"\u003ccenter\u003e\n身高 179cm\n\u003cdiv id=\"weightTable\"\u003e\u003c/div\u003e\n\u003c/center\u003e\n\u003cscript src=\"weight-table.js\"\u003e\u003c/script\u003e\n\u003ccanvas id=\"weightChart\" style=\"width:100%;margin:0 auto 2rem\"\u003e\u003c/canvas\u003e\n\u003cscript src=\"https://cdn.jsdelivr.net/npm/chart.js\"\u003e\u003c/script\u003e\n\u003cscript src=\"weight.js\"\u003e\u003c/script\u003e","title":"每日体重记录"},{"content":" 世界上最可笑的悲剧就是我已经告诉了你路怎么走，你却还在傻傻回头。\n最后更新日期：2026年6月30日 如果该日期太过久远，下面内容可能失效。带 ⏳ 标记的条目更新最快，下单/采用前建议再查当月榜单。\n产品选择 💻 软件 / 开发工具 类别 推荐 备注 Java 后端 IDE IntelliJ IDEA Ultimate 无争议。社区版够用但 Ultimate 的 Spring/数据库支持值回票价 轻量编辑器 / 全栈 VS Code 配合 Claude Code、远程开发 AI 编程助手 Claude Code 终端/IDE 内代理式编程 Windows 离线词典 GoldenDict-ng 开源、支持多词典格式（mdx/dsl/StarDict） 终端 (Windows) Windows Terminal + WSL2 你已在用 API 测试 Apifox（国内）/ Bruno（开源轻量） Postman 越来越臃肿，Bruno 是新趋势 数据库客户端 DataGrip / DBeaver DBeaver 免费且支持 PostGIS Git GUI Fork / GitKraken / 直接命令行 容器管理 OrbStack (Mac) / Docker Desktop OrbStack 在 Mac 上比 Docker Desktop 快很多 终端复用 / 远程 tmux + zellij 远程开发挂任务、断线重连必备 命令行效率三件套 fzf + ripgrep + zoxide 模糊查找 / 全局搜索 / 智能 cd，装完回不去 Shell 美化 Starship + zsh / fish 跨 shell 提示符，显示 git 状态、k8s 上下文 字体（编程） JetBrains Mono / Maple Mono Maple Mono 含中英等宽连字，国产精品 🤖 AI 工具 类别 推荐 备注 编程助手 ⏳ Claude Code / Claude Opus 复杂重构、agentic 工作流的第一梯队 国产对话模型 DeepSeek / 通义千问 / Kimi DeepSeek 性价比与代码能力强，你的数据管线已在用 本地推理 Ollama + Qwen / DeepSeek 蒸馏版 隐私敏感、离线场景；配合 NAS/ECS Java + AI 栈 Spring AI + pgvector RAG / agent 后端，2026 简历差异化最高 ROI 划词翻译 沉浸式翻译（浏览器插件） 双语对照，读英文文档/论文神器 📱 移动设备 类别 推荐 ⏳ 备注 性价比安卓神机 一加 Ace 5 系列 / iQOO Neo10 Pro / 红米 K80 魅族 21 已停产降级。当前 2-3K 价位这几款是主流共识 游戏性能机 iQOO Neo10 Pro / 红米 K80 Pro 双芯 + 大电池 影像旗舰 vivo X300 Pro / 小米 15 Ultra 拍照党首选 屏幕/生产力机皇 三星 S25 Ultra S Pen + 屏幕标杆 无广告/纯净系统 iPhone / 三星 国产 ROM 广告仍是减分项 平板 iPad（生态）/ 小米平板（性价比） 智能手表 Apple Watch（iOS）/ 华为 GT 系列（续航） 健康监测、久坐提醒 注：手机这块更新最快，建议下单前再查一次当月榜单。魅族 21 当年是\u0026quot;无广告 + 旗舰芯 + 直屏\u0026quot;的小众神机，但已过时。\n📷 影像 类别 推荐 备注 全画幅水桶机 索尼 A7M4 A7R3 已 2017 年老机型。A7M4 仍是\u0026quot;拍照为主、视频为辅\u0026quot;的性价比首选 全画幅新旗舰 索尼 A7M5（2025.12 发布） 30fps 连拍、7K 超采 4K60 无裁切，¥17999，预算够可上 高像素/风光 索尼 A7CR / A7R5 6100 万像素，A7CR 轻便（515g）适合街拍旅拍 入门全画幅 索尼 A7C2 学生党/记录党一次到位 Vlog 机 索尼 ZV-E1 全画幅视频旗舰 万金油镜头 适马 28-70 f2.8 DG DN / 腾龙 28-200 副厂性价比封神 后期软件 Lightroom + Capture One（按需） C1 直出色彩更好，LR 生态/同步更顺 存储卡 索尼 TOUGH / 闪迪 Extreme Pro A7R5/A7CR 高像素必上高速卡，别在卡上省钱 🎵 音乐 / 音频 类别 推荐 备注 流媒体（无损） Apple Music / Spotify / 网易云（曲库） Apple Music 无损 + 空间音频性价比高 本地音乐播放 foobar2000 / Navidrome（自建） Navidrome 可配合 NAS 自建私人流媒体 监听耳机 森海 HD 系列 / 拜雅 DT 系列 听音辨位、混音参考 通勤降噪 索尼 WH-1000XM 系列 / AirPods Pro XM 降噪标杆，AirPods 胜在生态 DAW（作曲） Logic Pro (Mac) / Reaper（轻量便宜） Reaper 一次买断、占用低，适合吉他/钢琴 demo 🖱️ 外设 类别 推荐 备注 性价比鼠标 罗技 G304 无线轻量经典 进阶无线鼠标 罗技 G Pro X Superlight 2 / MX Master 3S 电竞 vs 生产力 机械键盘 客制化 (VIA/QMK) / 黑爵、达尔优 国产客制化已成主流 显示器（编程） 4K 27\u0026quot; 或 2K 高刷 / 带鱼屏 多窗口需求 人体工学椅 西昊 / 永艺（国产性价比） 显示器支架臂 乐歌 / NB 解放桌面、保护颈椎 充电（出行） Anker / 倍思 氮化镓多口 一个头充满手机+笔记本+相机 移动电源 Anker / 小米（带 PD 快充） 65W+ 可给笔记本应急 💾 存储 / 自建基础设施 类别 推荐 备注 网盘 115 网盘（资源/秒传）+ 自建（隐私） 国内场景成立 自建网盘 Nextcloud / Seafile 隐私敏感数据 NAS 系统 群晖 DSM（省心）/ TrueNAS / 飞牛 fnOS（国产新秀） 你已在玩 NAS 同步工具 Syncthing 去中心化、跨设备 照片管理 Immich（自建，AI 相册） 替代 Google Photos 的最佳自建方案 密码管理 Vaultwarden（自建）/ Bitwarden（省心） Rust 写的轻量 Bitwarden，\u0026lt;50MB 内存，全客户端兼容 笔记 / 知识库 Obsidian（本地双链）/ 思源（数据库） Obsidian 本地 Markdown，配合 Git/NAS 同步 待办 / GTD Logseq（大纲）/ TickTick 滴答清单 大纲流 vs 传统清单流 内网穿透 Tailscale / frp 反向代理 Caddy（自动 HTTPS）/ Nginx 你在用 Nginx 监控告警 Uptime Kuma + ntfy 服务掉线推送，你已在用 ntfy RSS 阅读 FreshRSS（自建）+ 客户端 对抗信息茧房、聚合优质源 书签 / 稍后读 Karakeep（原 Hoarder）/ Linkwarden 自建书签，AI 自动打标签 容器管理面板 Dockge / Portainer Dockge 轻量、专注 compose 生活习惯 每天要保持 7 小时以上的稳定睡眠。\n最优饮食结构：蛋白 + 适量脂肪 + 控制超加工食品 (UPF)。\n吃水果 ≫ 喝果汁。\n每周至少 2-3 次力量训练 + 日常多走路。久坐是程序员头号杀手，比熬夜更隐蔽。\n护眼：屏幕 20-20-20 法则（每 20 分钟看 20 英尺外 20 秒）；显示器亮度跟环境匹配。\n自律带来的是自由，不是束缚。\n先完成，再完美。\n工作 / 工程实践 任何手动做过两次的操作，第三次就该写成脚本。\n备份遵循 3-2-1：3 份副本、2 种介质、1 份异地。没验证过能恢复的备份不算备份。\n提交信息写给三个月后的自己看，不是写给 commit 数量看。\n能在文档里讲清楚的，就别只存在脑子里。给项目写 README / CLAUDE.md 的时间永远值回票价。\n技术选型先问\u0026quot;能不能不引入\u0026quot;，再问\u0026quot;引入哪个\u0026quot;。每个依赖都是未来的债。\n人际交往 沟通的本质是对方听懂了什么，不是你说了什么。\n没人像你以为的那样在盯着你。在别人眼里你甚至微不足道。\n关系需要主动维护，不会自动续命。对于值得托付的朋友，隔三岔五找他玩玩。\n求助不丢人。憋着不问、自己硬扛才是真正的低效。\n内耗问题 能用钱解决的麻烦，就别耗自己的时间和情绪。\n大多数焦虑来自想得太多、做得太少。\n完成比完美更能治焦虑——动起来，多巴胺自然回来。\n定期清理：删掉不用的订阅、退掉无效的群、卸载吃时间的 App。物理和数字的断舍离都减轻内耗。\n把\u0026quot;应该\u0026quot;换成\u0026quot;选择\u0026quot;。是你选择做这件事，不是被它绑架。\n","permalink":"https://lv-blog.pages.dev/posts/life/best-practice/","summary":"\u003cblockquote\u003e\n\u003cp\u003e世界上最可笑的悲剧就是我已经告诉了你路怎么走，你却还在傻傻回头。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cstrong\u003e最后更新日期：2026年6月30日\u003c/strong\u003e\n如果该日期太过久远，下面内容可能失效。带 ⏳ 标记的条目更新最快，下单/采用前建议再查当月榜单。\u003c/p\u003e\n\u003ch2 id=\"产品选择\"\u003e产品选择\u003c/h2\u003e\n\u003ch3 id=\"-软件--开发工具\"\u003e💻 软件 / 开发工具\u003c/h3\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e类别\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e推荐\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e备注\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eJava 后端 IDE\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eIntelliJ IDEA Ultimate\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e无争议。社区版够用但 Ultimate 的 Spring/数据库支持值回票价\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e轻量编辑器 / 全栈\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eVS Code\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e配合 Claude Code、远程开发\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eAI 编程助手\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eClaude Code\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e终端/IDE 内代理式编程\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eWindows 离线词典\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eGoldenDict-ng\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e开源、支持多词典格式（mdx/dsl/StarDict）\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e终端 (Windows)\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eWindows Terminal + WSL2\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e你已在用\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eAPI 测试\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eApifox（国内）/ Bruno（开源轻量）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003ePostman 越来越臃肿，Bruno 是新趋势\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e数据库客户端\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDataGrip / DBeaver\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDBeaver 免费且支持 PostGIS\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eGit GUI\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eFork / GitKraken / 直接命令行\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e容器管理\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eOrbStack (Mac) / Docker Desktop\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eOrbStack 在 Mac 上比 Docker Desktop 快很多\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e终端复用 / 远程\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003etmux + zellij\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e远程开发挂任务、断线重连必备\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e命令行效率三件套\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003efzf + ripgrep + zoxide\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e模糊查找 / 全局搜索 / 智能 cd，装完回不去\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eShell 美化\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eStarship + zsh / fish\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e跨 shell 提示符，显示 git 状态、k8s 上下文\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e字体（编程）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eJetBrains Mono / Maple Mono\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eMaple Mono 含中英等宽连字，国产精品\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"-ai-工具\"\u003e🤖 AI 工具\u003c/h3\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e类别\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e推荐\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e备注\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e编程助手 ⏳\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eClaude Code / Claude Opus\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e复杂重构、agentic 工作流的第一梯队\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e国产对话模型\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDeepSeek / 通义千问 / Kimi\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eDeepSeek 性价比与代码能力强，你的数据管线已在用\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e本地推理\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eOllama + Qwen / DeepSeek 蒸馏版\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e隐私敏感、离线场景；配合 NAS/ECS\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eJava + AI 栈\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eSpring AI + pgvector\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eRAG / agent 后端，2026 简历差异化最高 ROI\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e划词翻译\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e沉浸式翻译（浏览器插件）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e双语对照，读英文文档/论文神器\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"-移动设备\"\u003e📱 移动设备\u003c/h3\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e类别\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e推荐 ⏳\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e备注\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e性价比安卓神机\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e一加 Ace 5 系列 / iQOO Neo10 Pro / 红米 K80\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e魅族 21 已停产降级。当前 2-3K 价位这几款是主流共识\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e游戏性能机\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eiQOO Neo10 Pro / 红米 K80 Pro\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e双芯 + 大电池\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e影像旗舰\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003evivo X300 Pro / 小米 15 Ultra\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e拍照党首选\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e屏幕/生产力机皇\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e三星 S25 Ultra\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eS Pen + 屏幕标杆\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e无广告/纯净系统\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eiPhone / 三星\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e国产 ROM 广告仍是减分项\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e平板\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eiPad（生态）/ 小米平板（性价比）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e智能手表\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eApple Watch（iOS）/ 华为 GT 系列（续航）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e健康监测、久坐提醒\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cblockquote\u003e\n\u003cp\u003e注：手机这块更新最快，建议下单前再查一次当月榜单。魅族 21 当年是\u0026quot;无广告 + 旗舰芯 + 直屏\u0026quot;的小众神机，但已过时。\u003c/p\u003e","title":"最佳实践"},{"content":"总入口\n个人博客 https://lv-blog.pages.dev/ ","permalink":"https://lv-blog.pages.dev/posts/misc/lv-roadmap/","summary":"\u003cp\u003e总入口\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e个人博客 \u003ca href=\"https://lv-blog.pages.dev/\"\u003ehttps://lv-blog.pages.dev/\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e","title":"网站路线图"},{"content":"待增加的功能：\n左下角音乐播放器 等内容足够丰富，将不再使用hugo框架 增加评论系统 增加浏览数统计系统 ","permalink":"https://lv-blog.pages.dev/posts/misc/blog-update-roadmap/","summary":"\u003cp\u003e待增加的功能：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e左下角音乐播放器\u003c/li\u003e\n\u003cli\u003e等内容足够丰富，将不再使用hugo框架\u003c/li\u003e\n\u003cli\u003e增加评论系统\u003c/li\u003e\n\u003cli\u003e增加浏览数统计系统\u003c/li\u003e\n\u003c/ul\u003e","title":"本博客程序更新路线"},{"content":"\n我要做一个名为 閾界事务所 的异常现象（包含各类不同寻常的案件）调查的项目。致力于收集各类异常现象，并且未来成长成为一个专门探索异常事件的社区。\n目前在项目的初级阶段，我希望先简历档案系统。将所有异常事件归类为一个档案。这个档案系统要存储各类异常事件。\n档案有两大类：民间档案 和 官方档案。 民间档案就是网友老百姓东拼西凑或者道听途说的，没有经过网站官方的验证。可信度一般比较飘。 官方档案就是经过官方验证确有此事的。可信度最高。\n我的前端要在手机、网页都可以展示。目前先用flutter做好移动端。\n我的后端系统的数据库存储档案应该以什么形式？我用什么数据库存储？ 文本格式是markdown还是quill？\n我的文本有图片有音频有视频。媒体文件我可以用 bucket\n未来要实现的功能： 每个档案会出现人物关系图 警方是谁，谁是嫌疑人，等等 每个档案会有“时序图”，简明扼要论述该事件发生的经过 每个档案会有事件发生的坐标，方便未来可以在大地图上显示某地某地有异常事件发生\n唯心主义的迷雾，唯物论者的悬崖\n","permalink":"https://lv-blog.pages.dev/posts/projects/lia/%E9%96%BE%E7%95%8C%E4%BA%8B%E5%8A%A1%E6%89%80%E9%A1%B9%E7%9B%AE%E4%B8%80%E4%B8%AA%E5%BC%82%E5%B8%B8%E6%97%B6%E9%97%B4%E7%88%B1%E5%A5%BD%E8%80%85%E7%9A%84%E8%81%9A%E9%9B%86%E5%9C%B0/","summary":"\u003cp\u003e\u003cimg alt=\"事务所建筑\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/e6bf0a73fd4c5534ce17efea5d9502c88e5787936c1213eae81255130b22667b.jpg\"\u003e\u003c/p\u003e\n\u003cp\u003e我要做一个名为 閾界事务所 的异常现象（包含各类不同寻常的案件）调查的项目。致力于收集各类异常现象，并且未来成长成为一个专门探索异常事件的社区。\u003c/p\u003e\n\u003cp\u003e目前在项目的初级阶段，我希望先简历档案系统。将所有异常事件归类为一个档案。这个档案系统要存储各类异常事件。\u003c/p\u003e\n\u003cp\u003e档案有两大类：民间档案 和 官方档案。\n民间档案就是网友老百姓东拼西凑或者道听途说的，没有经过网站官方的验证。可信度一般比较飘。\n官方档案就是经过官方验证确有此事的。可信度最高。\u003c/p\u003e\n\u003cp\u003e我的前端要在手机、网页都可以展示。目前先用flutter做好移动端。\u003c/p\u003e\n\u003cp\u003e我的后端系统的数据库存储档案应该以什么形式？我用什么数据库存储？\n文本格式是markdown还是quill？\u003c/p\u003e\n\u003cp\u003e我的文本有图片有音频有视频。媒体文件我可以用 bucket\u003c/p\u003e\n\u003cp\u003e未来要实现的功能：\n每个档案会出现人物关系图\n警方是谁，谁是嫌疑人，等等\n每个档案会有“时序图”，简明扼要论述该事件发生的经过\n每个档案会有事件发生的坐标，方便未来可以在大地图上显示某地某地有异常事件发生\u003c/p\u003e\n\u003chr\u003e\n\u003cp\u003e\u003cem\u003e唯心主义的迷雾，唯物论者的悬崖\u003c/em\u003e\u003c/p\u003e","title":"閾界事务所项目：一个异常时间爱好者的聚集地"},{"content":"\n项目介绍 閾界档案室是一个针对异常现象爱好者开发的一个网站。收集全世界各类异常事件的资料。这些“异常事件”包括但不限于：\n各类刑事案件 各类灵异事件 各类失踪事件 唯心主义的迷雾，唯物论者的悬崖\n","permalink":"https://lv-blog.pages.dev/posts/projects/lia/%E9%96%BE%E7%95%8C%E4%BA%8B%E5%8A%A1%E6%89%80%E9%A1%B9%E7%9B%AE%E5%AD%90%E9%A1%B9%E7%9B%AE%E9%98%88%E7%95%8C%E6%A1%A3%E6%A1%88%E5%AE%A4%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/","summary":"\u003cp\u003e\u003cimg alt=\"Gemini_Generated_Image_1kpvj91kpvj91kpv\" loading=\"lazy\" src=\"https://img.wathan.cn/images/2026/06/1a52678baea7872da31ad02e8760f9adaafd6c6453c5a50f8fa18ed1515d5445.jpg\"\u003e\u003c/p\u003e\n\u003ch2 id=\"项目介绍\"\u003e项目介绍\u003c/h2\u003e\n\u003cp\u003e閾界档案室是一个针对异常现象爱好者开发的一个网站。收集全世界各类异常事件的资料。这些“异常事件”包括但不限于：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e各类刑事案件\u003c/li\u003e\n\u003cli\u003e各类灵异事件\u003c/li\u003e\n\u003cli\u003e各类失踪事件\u003c/li\u003e\n\u003cli\u003e\n\u003chr\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cem\u003e唯心主义的迷雾，唯物论者的悬崖\u003c/em\u003e\u003c/p\u003e","title":"閾界事务所项目：子项目：阈界档案室微信小程序"},{"content":"希望对缅甸不了解的朋友可以读一读。\n从被西方世界殖民，到日本的屠杀暴行。从国家无休止的内战，到毁灭性的自然灾害。缅甸这个国家背负了一个国家所能遭受的几乎所有的不幸，甚至和旧中国有几分相似。除此以外，现在一些中国人趁着缅甸政治局势的动荡，中央对边境管辖的局限，到缅甸北部搞电信诈骗。这本不是缅甸人民的所作所为，也被扣上了一个危险国家的帽子。国内不明真相的网民们跟风吆喝，似乎把缅甸描述成了一个地狱，给它贴上了所有黑色的标签。\n这就像夜晚一个村子里的恶霸逃了出来，到另一个村的边境路上坑蒙拐骗路自己原住村的居民。坑蒙拐骗得手了之后就藏起来。大家不讨论怎么铲除村里恶霸，而是对另一个村庄的所有人们恶语相向。这失智的群众，恶臭的三观，离谱的操作，荒诞的狗血情节。\n缅甸人能够修好多座大金塔，每个人都能参观。仰光的大金塔的修建更是用了数十吨的黄金，塔顶的伞盖镶嵌着大量的宝石。按黄金的最近翻倍价格，如果缅甸人真如大家贴的标签那样利欲熏心，坑蒙拐骗，无法靠近，想必早大金塔被当地的窃贼逐渐磨平，甚至被出卖。作为信奉佛教人数比例最高的国家之一，缅甸人信仰的佛法，佛道，对佛经的理解，早已是毕生必须经历的修行。对佛的侮辱，甚至甚过违犯法律。军政府在当地的统治，军阀在各地的割据，可以自己改法，吸血压榨自己的人民，枪毙侮辱军阀的百姓，但是唯独不能摧毁每个人心目中的释迦牟尼。\n请你不要被大数据的推送、夸张的短视频被遮住了眼睛，也别忘了去倾听那些沉默却坚定的声音。缅甸并不是只有战争、诈骗与苦难，她也有善良、虔诚与坚韧。这个国家的光亮，可能微弱，甚至你从抵达仰光机场的飞机向下俯瞰也不见几处灯火，但它的希望始终没有熄灭。每个缅甸人的内心和大家一样朴素，在乎的是自己的下一餐是否是美食，天冷了自己的孩子今天有没有受凉，商店里的牛奶有没有打折，以及明天，是不是会变得更好。\n我相信不管缅甸会在未来遇到多大的困难，她总会再站起来。一个国家甚至一个民族的坚强，不在于她遭受到了多么毁灭性的打击，而是在于她总是能在最恶劣的环境下抬起头来，面向阳光，茁壮成长。\n","permalink":"https://lv-blog.pages.dev/posts/thoughts/things-about-myanmar/","summary":"\u003cp\u003e希望对缅甸不了解的朋友可以读一读。\u003c/p\u003e\n\u003cp\u003e从被西方世界殖民，到日本的屠杀暴行。从国家无休止的内战，到毁灭性的自然灾害。缅甸这个国家背负了一个国家所能遭受的几乎所有的不幸，甚至和旧中国有几分相似。除此以外，现在一些中国人趁着缅甸政治局势的动荡，中央对边境管辖的局限，到缅甸北部搞电信诈骗。这本不是缅甸人民的所作所为，也被扣上了一个危险国家的帽子。国内不明真相的网民们跟风吆喝，似乎把缅甸描述成了一个地狱，给它贴上了所有黑色的标签。\u003c/p\u003e\n\u003cp\u003e这就像夜晚一个村子里的恶霸逃了出来，到另一个村的边境路上坑蒙拐骗路自己原住村的居民。坑蒙拐骗得手了之后就藏起来。大家不讨论怎么铲除村里恶霸，而是对另一个村庄的所有人们恶语相向。这失智的群众，恶臭的三观，离谱的操作，荒诞的狗血情节。\u003c/p\u003e\n\u003cp\u003e缅甸人能够修好多座大金塔，每个人都能参观。仰光的大金塔的修建更是用了数十吨的黄金，塔顶的伞盖镶嵌着大量的宝石。按黄金的最近翻倍价格，如果缅甸人真如大家贴的标签那样利欲熏心，坑蒙拐骗，无法靠近，想必早大金塔被当地的窃贼逐渐磨平，甚至被出卖。作为信奉佛教人数比例最高的国家之一，缅甸人信仰的佛法，佛道，对佛经的理解，早已是毕生必须经历的修行。对佛的侮辱，甚至甚过违犯法律。军政府在当地的统治，军阀在各地的割据，可以自己改法，吸血压榨自己的人民，枪毙侮辱军阀的百姓，但是唯独不能摧毁每个人心目中的释迦牟尼。\u003c/p\u003e\n\u003cp\u003e请你不要被大数据的推送、夸张的短视频被遮住了眼睛，也别忘了去倾听那些沉默却坚定的声音。缅甸并不是只有战争、诈骗与苦难，她也有善良、虔诚与坚韧。这个国家的光亮，可能微弱，甚至你从抵达仰光机场的飞机向下俯瞰也不见几处灯火，但它的希望始终没有熄灭。每个缅甸人的内心和大家一样朴素，在乎的是自己的下一餐是否是美食，天冷了自己的孩子今天有没有受凉，商店里的牛奶有没有打折，以及明天，是不是会变得更好。\u003c/p\u003e\n\u003cp\u003e我相信不管缅甸会在未来遇到多大的困难，她总会再站起来。一个国家甚至一个民族的坚强，不在于她遭受到了多么毁灭性的打击，而是在于她总是能在最恶劣的环境下抬起头来，面向阳光，茁壮成长。\u003c/p\u003e","title":"缅甸：金塔未倾，佛心未泯——写给不明真相的中国朋友"},{"content":"长相失败， 化妆无奈。 皮黑耐晒， 口音奇怪。 腿粗缺钙， 不会做菜。 如果被爱， 纯属意外。\n","permalink":"https://lv-blog.pages.dev/posts/essay/random-8-words/","summary":"\u003cp\u003e长相失败，\n化妆无奈。\n皮黑耐晒，\n口音奇怪。\n腿粗缺钙，\n不会做菜。\n如果被爱，\n纯属意外。\u003c/p\u003e","title":"无奈八则"},{"content":"\n📥下载MSCZ格式曲谱\n📥下载PDF格式曲谱\n","permalink":"https://lv-blog.pages.dev/posts/piano/musicsheet/arrangement-musicsheet-the-second-dance/","summary":"\u003cp\u003e\u003ca href=\"https://musescore.com/user/30983934/scores/5892865?from=notification\"\u003e\u003cimg alt=\"MuseScore Page\" loading=\"lazy\" src=\"https://img.shields.io/badge/MuseScore-Page-0078d7?style=for-the-badge\u0026logo=musescore\u0026logoColor=white\"\u003e\u003c/a\u003e\u003c/p\u003e\n\u003cdiv class=\"pdf-container\" style=\"position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; margin: 20px 0;\"\u003e\n    \u003ciframe \n        src=\"https://img.wathan.cn/the-second-dance-from-movie-hachi-a-dogs-tale.pdf\" \n        width=\"100%\" \n        height=\"100%\" \n        style=\"position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none;\"\n        allow=\"fullscreen\"\u003e\n    \u003c/iframe\u003e\n\u003c/div\u003e\n\u003cp\u003e📥\u003ca href=\"https://img.wathan.cn/the-second-dance-from-movie-hachi-a-dogs-tale.mscz\"\u003e下载MSCZ格式曲谱\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e📥\u003ca href=\"https://img.wathan.cn/the-second-dance-from-movie-hachi-a-dogs-tale.pdf\"\u003e下载PDF格式曲谱\u003c/a\u003e\u003c/p\u003e","title":"The Second Dance 改编谱"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/work/resume/","summary":"\u003cdiv class=\"pdf-container\" style=\"height:80vh;margin: 20px 0;border:none\"\u003e\n    \u003ciframe \n        src=\"/posts/work/resume.pdf\" \n        width=\"100%\" \n        height=\"100%\" \n        style=\"border:none\"\n        allow=\"fullscreen\"\u003e\n    \u003c/iframe\u003e\n\u003c/div\u003e","title":"个人简历"},{"content":"你理解这段代码吗？\ndo { oldseed = seed.get(); newseed = (oldseed * ...) \u0026amp; mask; } while (!seed.compareAndSet(oldseed, newseed)); // ← CAS 第〇层：先搞懂\u0026quot;变量\u0026quot;在多线程里的危险 假设有一个变量 seed = 100，两个线程同时想改它：\n线程A：读到 seed=100 → 算出新值 200 → 写回 seed=200 线程B：读到 seed=100 → 算出新值 300 → 写回 seed=300 问题来了——两个人都读到了 100，各算各的，最后谁写得晚谁赢。A 的结果被 B 覆盖了，A 白干了，而且程序根本不知道出错了。\n这就是经典的 线程安全问题。\n第一层：解决办法有两条路 路线一：悲观锁（synchronized） → \u0026#34;我先把门锁上，别人别进来\u0026#34; → 安全但慢（别人要排队） 路线二：乐观锁（CAS） → \u0026#34;门不锁，但我写之前验一下，被人改了我就重来\u0026#34; → 快但可能要重试 你这段代码走的就是 路线二。\n第二层：什么是 CAS？ CAS = Compare And Set（比较并设置），三个参数：\nCAS(内存地址, 我期望的旧值, 我想写入的新值) 它做的事用大白话说：\n\u0026ldquo;我记得这个值是 42，如果现在还是 42，就帮我改成 77。如果不是 42 了，说明有人动过了，啥也别干，告诉我失败了。\u0026rdquo;\n关键点：这整个\u0026quot;比较+写入\u0026quot;是一条 CPU 指令完成的，不可能被打断。这就是它能保证线程安全的根本原因。\n第三层：逐行拆代码 do { oldseed = seed.get(); // ① 拍快照 newseed = (oldseed * ...) \u0026amp; mask; // ② 算新值 } while (!seed.compareAndSet(oldseed, newseed)); // ③ CAS 尝试写入 ① seed.get()\nseed 是一个 AtomicLong，它的 get() 就是读当前值。把当前值存到 oldseed 里，相当于拍了张照片——\u0026ldquo;我看到的是这个值\u0026rdquo;。\n② 算新值\n基于旧值做运算。这里是伪随机数的公式（线性同余），细节不重要，关键是：新值依赖旧值。\n③ compareAndSet(oldseed, newseed)\n这就是 CAS 操作：\n比较：seed 的当前值还是 oldseed 吗？ 是 → 把 seed 改成 newseed，返回 true 不是 → 什么也不做，返回 false ④ while(!...) 自旋\nCAS 返回 true → !true = false → 跳出循环，搞定。 CAS 返回 false → !false = true → 继续循环，从 ① 重新来。\n第四层：用生活场景走一遍 flowchart TD A[\"开始\"] --\u003e B[\"① 读取当前值 oldseed = seed.get() 比如读到 100\"] B --\u003e C[\"② 计算新值 newseed = f(100) = 777\"] C --\u003e D{\"③ CAS 验证 seed 现在还是 100 吗？\"} D --\u003e|\"是 → 没人动过 写入 777，返回 true\"| E[\"成功！跳出循环\"] D --\u003e|\"不是 → 被别人改了 返回 false\"| F[\"白算了，重新来\"] F --\u003e B 第五层：两个线程抢的时候到底发生了什么？ 时间线：\n时刻1: seed = 100 时刻2: 线程A 读到 oldseed=100 时刻3: 线程B 读到 oldseed=100 时刻4: 线程A 算出 newseed=777 时刻5: 线程B 算出 newseed=888 时刻6: 线程A 执行 CAS(100, 777) → seed还是100吗？是！→ 改成777 → 成功 ✅ 时刻7: 线程B 执行 CAS(100, 888) → seed还是100吗？不是了，已经是777了 → 失败 ❌ 时刻8: 线程B 重新来一轮 → 读到 oldseed=777 → 算出 newseed=999 → CAS(777, 999) → 成功 ✅ 注意：没有任何数据被覆盖，没有任何结果丢失。这就是 CAS 的安全保障。\n第六层：为什么不直接加锁？ 加锁 synchronized CAS 自旋 ───────────────────────────────────────────────────── 有人占着时 线程挂起（睡觉） 线程空转（反复重试） 恢复代价 唤醒线程 ≈ 几微秒 无代价，下一轮循环就行 竞争少时 锁的开销浪费了 极快，几乎零开销 竞争多时 排队等，稳定 空转浪费 CPU 适合场景 操作耗时长 操作极快（纳秒级） 这段代码里的操作就是一次乘法和一次位运算，纳秒级别就完事了，所以用 CAS 比加锁快得多。\n第七层：这段代码到底是干嘛的？ 这段代码来自 Java 的 java.util.Random 或 ThreadLocalRandom，它在生成伪随机数。\nseed 就是随机数的\u0026quot;种子\u0026quot;，每次调用 nextInt() 都要：\n拿到当前种子 用种子算出下一个种子（线性同余公式） 用 CAS 把种子更新上去 基于新种子算出随机数返回给你 用 CAS 是因为多个线程可能同时在调 nextInt()，必须保证每个线程拿到的种子不一样，否则会生成重复的\u0026quot;随机\u0026quot;数。\n","permalink":"https://lv-blog.pages.dev/posts/programming/backend/understanding-cas-spin-from-random/","summary":"\u003cp\u003e你理解这段代码吗？\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003edo\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    oldseed \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e seed.\u003cspan style=\"color:#a6e22e\"\u003eget\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    newseed \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e (oldseed \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e ...) \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003e mask;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e} \u003cspan style=\"color:#66d9ef\"\u003ewhile\u003c/span\u003e (\u003cspan style=\"color:#f92672\"\u003e!\u003c/span\u003eseed.\u003cspan style=\"color:#a6e22e\"\u003ecompareAndSet\u003c/span\u003e(oldseed, newseed));   \u003cspan style=\"color:#75715e\"\u003e// ← CAS\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"第层先搞懂变量在多线程里的危险\"\u003e第〇层：先搞懂\u0026quot;变量\u0026quot;在多线程里的危险\u003c/h2\u003e\n\u003cp\u003e假设有一个变量 \u003ccode\u003eseed = 100\u003c/code\u003e，两个线程\u003cstrong\u003e同时\u003c/strong\u003e想改它：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e线程A：读到 seed=100 → 算出新值 200 → 写回 seed=200\n线程B：读到 seed=100 → 算出新值 300 → 写回 seed=300\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e问题来了——两个人都读到了 100，各算各的，最后谁写得晚谁赢。A 的结果被 B 覆盖了，A 白干了，而且\u003cstrong\u003e程序根本不知道出错了\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e这就是经典的 \u003cstrong\u003e线程安全问题\u003c/strong\u003e。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"第一层解决办法有两条路\"\u003e第一层：解决办法有两条路\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e路线一：悲观锁（synchronized）\n  → \u0026#34;我先把门锁上，别人别进来\u0026#34;\n  → 安全但慢（别人要排队）\n\n路线二：乐观锁（CAS）\n  → \u0026#34;门不锁，但我写之前验一下，被人改了我就重来\u0026#34;\n  → 快但可能要重试\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e你这段代码走的就是 \u003cstrong\u003e路线二\u003c/strong\u003e。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"第二层什么是-cas\"\u003e第二层：什么是 CAS？\u003c/h2\u003e\n\u003cp\u003eCAS = \u003cstrong\u003eCompare And Set\u003c/strong\u003e（比较并设置），三个参数：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eCAS(内存地址, 我期望的旧值, 我想写入的新值)\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e它做的事用大白话说：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u0026ldquo;我记得这个值是 42，如果现在\u003cstrong\u003e还是\u003c/strong\u003e 42，就帮我改成 77。如果不是 42 了，说明有人动过了，啥也别干，告诉我失败了。\u0026rdquo;\u003c/p\u003e","title":"Java 从Random理解 CAS自旋模式"},{"content":"假设配偶是外国人，女方。\n女方境外人员住宿登记表 到当地派出所领取\n女方护照主页复印件\n（如果有）女方护照内上次居留许可复印件\n男方邀请函（移民管理局照相馆可提供）\n男方户口本复印件\n双方结婚证\n南方身份证复印件\n女方印刷出来的小照片（移民管理局照相馆可提供）\n提交所有内容到当地出入境移民管理局\n","permalink":"https://lv-blog.pages.dev/posts/visa/what-u-need-to-have-a-resident-permit-in-china/","summary":"\u003cp\u003e假设配偶是外国人，女方。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e女方境外人员住宿登记表\n到当地派出所领取\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e女方护照主页复印件\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e（如果有）女方护照内上次居留许可复印件\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e男方邀请函（移民管理局照相馆可提供）\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e男方户口本复印件\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e双方结婚证\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e南方身份证复印件\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e女方印刷出来的小照片（移民管理局照相馆可提供）\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e提交所有内容到当地出入境移民管理局\u003c/p\u003e","title":"外国人在中国镇江通过结婚证申请居留许可或续签需要的材料"},{"content":"缓存三兄弟 缓存穿透 是什么：大量请求查询数据库不存在的数据。缓存没有，持续击打数据库 防范方法：\n缓存空值 数据库查不到也往缓存放一个空标记（TTL短，2分钟，该TTL过期了才能进行再次数据库查询） 下一次同样的id来了直接被挡住 优点：有效拦截大量穿透请求 缺点： 恶意攻击造成大量不同的不存在的key，缓存堆满无效数据，浪费内存 延迟了数据一致性，新数据必须等缓存过期 布隆过滤器（推荐） 启动时把所有合法id放入布隆过滤器； 布隆过滤器不给进就真的没有； 优点： 没有高内存占用风险 没有数据一致性风险 缺点： 有极小误判率 增删要维护。数据更新时，要同步更新布隆过滤器 接口层限流和校验 API网关或Controller层做拦截 参数校验、限流降级 互斥锁重建 互斥锁主要解决缓存击穿（热点key过期），但是在缓存空对象场景下，如果有大量并发请求一个不存在的key，可以使用锁 每次访问让一个线程去查DB，其他线程等待 大厂的实践\n网关层 校验参数格式，IP限流（挡掉脚本攻击） 过滤器层 布隆过滤器 缓存层 缓存击穿 是什么：某个被疯狂访问的热点key，因为TTL一到，失效的一瞬间海量并发请求同时miss，同时到DB重建同一个key\n防范方法：\n互斥锁 只让第1个miss的线程去查DB 其他线程等一下再读缓存 高一致性：线程傻傻等待 低一致性：先给旧数据，下一次来查就是新的 逻辑过期 key永不物理过期，把过期时间存在value里面 如果发现逻辑上过期了，异步开个线程重建 当前请求先返回旧值 互斥锁的实现 如何搭建出正确的锁？\nv1：SETNX key 1 抢锁，DEL key 释放\nSETNX=\u0026ldquo;不存在才设置成功\u0026rdquo;，天然互斥。但问题：抢到锁的线程崩了、DEL 没执行 → 锁永远不释放 → 死锁。\nv2：给锁加过期间\n崩了也能自动过期。但关键：必须用一条原子命令 SET key value NX EX 30。\n陷阱：别写成 SETNX + 再 EXPIRE 两条——如果刚 SETNX 成功、还没 EXPIRE 就崩了，又变回 v1 的死锁。\u0026ldquo;抢锁\u0026quot;和\u0026quot;设过期\u0026quot;必须一步做完。\nv3 陷阱：误删别人的锁\n场景：A 抢到锁、TTL 30s，但 A 干活干了 35s。第 30s 锁自动过期 → B 抢到了同一把锁 → 第 35s，A 干完了、DEL key → 把 B 的锁删了。于是 B、C\n可能同时持锁，互斥失效。\n修法：抢锁时写一个唯一标识（比如 UUID）当 value，释放时先检查 value 是不是自己的，是自己的才删。\nv4 陷阱：检查和删除不是原子的\nGET key 确认是自己 → DEL key，这是两步。万一 GET 之后、DEL 之前，锁恰好过期、被 C 抢走，你的 DEL 又把 C 的删了。\n修法：用 Lua 脚本把\u0026quot;比对 value + 删除\u0026quot;打包成一条原子操作（Redis 单线程执行lua，中间插不进别的命令）。\nv5 及以上（了解）\n看门狗续期：任务可能比 TTL 长，怎么办？后台起个线程定期给锁续期，快过期就延长——Redisson\n的看门狗就是干这个的。 Redisson：生产里基本不手写，直接用 Redisson 的 RLock（封装了上面所有坑 + 可重入 + 看门狗）。 红锁 RedLock：单点 Redis 主从切换会丢锁，红锁想用多个独立节点解决，但业界有争议（Martin Kleppmann 那场著名辩论）。面试能说出\u0026quot;知道有红锁、也知道它有争议、生产我会用Redisson\u0026quot;就到位了。 缓存雪崩 是什么：大批key同一时刻集体过期。 场景：\n系统启动时一次性灌满了一堆缓存，TTL都一样 Redis整个挂了，海量请求同时打到DB 和击穿的区别：击穿是一个热点key；雪崩是一大片key\n防范方法：\nTTL加随机值 Redis高可用 主从 哨兵 集群 不让redis单点挂掉引发雪崩 多级缓存+限流降级 本地缓存先扛一层 最后用熔断限流保护 Cache-Aside 旁路缓存 适用的场景：所有读多写少的情况 实际业务：用户的资料信息、商品详情页等 决策口诀：“如果这个数据，用户刷新一次页面就要查一次，但用户一天只会改一次，那么闭眼用 Cache-Aside。” 作用过程：读时填充，写时失效\n案例1：聊天室在线列表。 很多人同时查询在线列表。不能都去数据库读。 将在线列表缓存到 Redis 中。查询直接查 Redis。\n聊天室人数增加/减少 -\u0026gt; 触发写DB -\u0026gt; 删除缓存 与此同时， 查询聊天室在线列表 -\u0026gt; 缓存失效 -\u0026gt; 到DB查 -\u0026gt; 将DB写入缓存\n","permalink":"https://lv-blog.pages.dev/posts/programming/backend/redis/common-cache-strategies/","summary":"\u003ch1 id=\"缓存三兄弟\"\u003e缓存三兄弟\u003c/h1\u003e\n\u003ch2 id=\"缓存穿透\"\u003e缓存穿透\u003c/h2\u003e\n\u003cp\u003e是什么：大量请求查询数据库不存在的数据。缓存没有，持续击打数据库\n防范方法：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e缓存空值\n\u003col\u003e\n\u003cli\u003e数据库查不到也往缓存放一个空标记（TTL短，2分钟，该TTL过期了才能进行再次数据库查询）\u003c/li\u003e\n\u003cli\u003e下一次同样的id来了直接被挡住\u003c/li\u003e\n\u003cli\u003e优点：有效拦截大量穿透请求\u003c/li\u003e\n\u003cli\u003e缺点：\n\u003col\u003e\n\u003cli\u003e恶意攻击造成大量不同的不存在的key，缓存堆满无效数据，浪费内存\u003c/li\u003e\n\u003cli\u003e延迟了数据一致性，新数据必须等缓存过期\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e布隆过滤器\u003c/strong\u003e（推荐）\n\u003col\u003e\n\u003cli\u003e启动时把所有合法id放入布隆过滤器；\u003c/li\u003e\n\u003cli\u003e布隆过滤器不给进就真的没有；\u003c/li\u003e\n\u003cli\u003e优点：\n\u003col\u003e\n\u003cli\u003e没有高内存占用风险\u003c/li\u003e\n\u003cli\u003e没有数据一致性风险\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/li\u003e\n\u003cli\u003e缺点：\n\u003col\u003e\n\u003cli\u003e有极小误判率\u003c/li\u003e\n\u003cli\u003e增删要维护。数据更新时，要同步更新布隆过滤器\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/li\u003e\n\u003cli\u003e接口层限流和校验\n\u003col\u003e\n\u003cli\u003eAPI网关或Controller层做拦截\u003c/li\u003e\n\u003cli\u003e参数校验、限流降级\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/li\u003e\n\u003cli\u003e互斥锁重建\n\u003col\u003e\n\u003cli\u003e互斥锁主要解决缓存击穿（热点key过期），但是在缓存空对象场景下，如果有大量并发请求一个不存在的key，可以使用锁\u003c/li\u003e\n\u003cli\u003e每次访问让一个线程去查DB，其他线程等待\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e大厂的实践\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e网关层 校验参数格式，IP限流（挡掉脚本攻击）\u003c/li\u003e\n\u003cli\u003e过滤器层 布隆过滤器\u003c/li\u003e\n\u003cli\u003e缓存层\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"缓存击穿\"\u003e缓存击穿\u003c/h2\u003e\n\u003cp\u003e是什么：某个被疯狂访问的热点key，因为TTL一到，失效的一瞬间海量并发请求同时miss，同时到DB重建同一个key\u003c/p\u003e\n\u003cp\u003e防范方法：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e互斥锁\n\u003col\u003e\n\u003cli\u003e只让第1个miss的线程去查DB\u003c/li\u003e\n\u003cli\u003e其他线程等一下再读缓存\n\u003col\u003e\n\u003cli\u003e高一致性：线程傻傻等待\u003c/li\u003e\n\u003cli\u003e低一致性：先给旧数据，下一次来查就是新的\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/li\u003e\n\u003cli\u003e逻辑过期\n\u003col\u003e\n\u003cli\u003ekey永不物理过期，把过期时间存在value里面\u003c/li\u003e\n\u003cli\u003e如果发现逻辑上过期了，异步开个线程重建\u003c/li\u003e\n\u003cli\u003e当前请求先返回旧值\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"互斥锁的实现\"\u003e互斥锁的实现\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e如何搭建出正确的锁？\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ev1：SETNX key 1 抢锁，DEL key 释放\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003eSETNX=\u0026ldquo;不存在才设置成功\u0026rdquo;，天然互斥。但问题：抢到锁的线程崩了、DEL 没执行 → 锁永远不释放 → 死锁。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003ev2：给锁加过期间\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e崩了也能自动过期。但关键：必须用一条原子命令 SET key value NX EX 30。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e陷阱：别写成 SETNX + 再 EXPIRE 两条——如果刚 SETNX 成功、还没 EXPIRE 就崩了，又变回 v1 的死锁。\u0026ldquo;抢锁\u0026quot;和\u0026quot;设过期\u0026quot;必须一步做完。\u003c/p\u003e","title":"常见缓存策略精析"},{"content":"基础知识 TCP 三次握手 客户端 - 服务器\n三次握手目的：客户端的收发没问题，服务器的收发也没问题。\n第一次握手：客户端发 SYN（同步请求） 客户端：我啥也不知道，我发个 SYN 消息出去，看看有没有人接收到。 如果没人回复，我再发几次（超时重传）。 这个时刻，客户端知道的事实是： 我发出去了，但不知道有没有人收到 ❌ 我能不能收消息？不知道 ❌ 对方存不存在？不知道 ❌ 第二次握手：服务器回 SYN+ACK（同步+确认） 服务器收到了客户端的 SYN。\n服务器回复 SYN+ACK：“我收到你的消息了，我也发一条试试，看看你能不能收到。”\n这个时刻，服务器知道的事实是：\n客户端发消息是没有问题的 ✅（因为我收到了） 服务器收消息是没有问题的 ✅（因为我收到了） 服务器不知道服务器发消息有没有问题 ❌（我发出去了，但不知道对方收到没） 服务器不知道客户端收消息有没有问题 ❌（我发出去了，但不知道对方收到没） 服务器需要发送一条 SYN+ACK，等对方回复 ACK 来确认“我能发、对方能收” 当客户端收到 SYN+ACK 后，客户端可以确定以下事实：\n我发消息是没有问题的 ✅（我发的 SYN 被收到了） 我收消息也是没有问题的 ✅（我收到了对方的回复） 服务器收消息是没有问题的 ✅（对方收到了我的 SYN） 服务器发消息也是没有问题的 ✅（我收到了对方的 SYN+ACK） ✅ 客户端这边已经确认全双工通信没问题了！ 但是服务器他还有顾虑（不知道自己能不能发、不知道我能不能收），我再发个 ACK 给他吧 第三次握手：客户端发 ACK（确认） 客户端收到 SYN+ACK 后，回复一个 ACK：“我收到你的 SYN+ACK 了，我这边收发都没问题，你也可以放心了。”\n当服务器收到这个 ACK 后，服务器可以确定以下事实：\n客户端发消息是没有问题的 ✅（第一次握手已验证） 服务器收消息是没有问题的 ✅（第一次握手已验证） 服务器发消息也是没有问题的 ✅（我发的 SYN+ACK 被客户端收到了） 客户端收消息也是没有问题的 ✅（我发的 SYN+ACK 被客户端收到了，他能收到就说明他能收） ✅ 服务器这边也确认全双工通信没问题了！ 随后双方进入 ESTABLISHED 状态，可以开始传输正式数据。\n⚠️ 如果第三次握手的 ACK 丢了怎么办？ 客户端视角：我发了 ACK，也直接发了正式数据（因为客户端认为连接已建立）。 服务器视角：我没收到 ACK，我还处于 SYN-RCVD 状态，我不确定自己能不能发、客户端能不能收。 服务器行为：超时后重传 SYN+ACK（有限次数）。 客户端收到重传的 SYN+ACK 后： 客户端意识到“哦，我上次的 ACK 可能丢了” 客户端再次发送 ACK，并且捎带重传之前发的正式数据（如果有的话） 如果正式数据先到达服务器： 服务器的重传计时器还没超时，收到了客户端发来的正式数据包 这个数据包的 TCP 头部携带了 ACK 标志，正好确认了第二次握手 服务器立刻将连接状态转为 ESTABLISHED，收下数据 如果双方都超时了： 服务器有限次数重传 SYN+ACK 后无回应 → 放弃连接，释放资源 客户端有限次数重传正式数据后无回应 → 放弃连接，通知上层应用 双方都回到 CLOSED 状态 🤔 为什么不能只握手两次？ 如果只有两次握手（客户端发 SYN，服务器回 SYN+ACK 就结束）：\n角色 知道的事实 不知道的事实 客户端 我发没问题 ✅\n我收没问题 ✅\n服务器发没问题 ✅\n服务器收没问题 ✅ 全部确认 ✅ 服务器 客户端发没问题 ✅\n服务器收没问题 ✅ 服务器发没问题 ❌\n客户端收没问题 ❌ 后果：服务器在不确定自己能不能发、客户端能不能收的情况下就开始发送正式数据。如果服务器的 SYN+ACK 实际上客户端没收到，服务器还在傻傻地发数据，客户端根本收不到，导致死锁。\n三次握手的本质：第三次 ACK 是给服务器吃的定心丸——让服务器确认“我能发，对方能收”，然后才敢放心发数据。\n🤔 三次握手能携带数据吗？ 第一次握手（SYN）：不能携带数据。\n因为此时连接还没建立，客户端还不知道服务器存不存在 如果携带数据，万一 SYN 丢了，数据白发了，且容易引发 SYN 攻击 第二次握手（SYN+ACK）：不能携带数据。\n服务器还没收到客户端的最终确认，不确定自己能不能发、客户端能不能收 如果带数据，万一 ACK 丢了，数据白发了 第三次握手（ACK）：可以携带数据（RFC 793 允许）。\n客户端已经确认了双方收发能力，可以放心捎带正式数据 如果 ACK 丢了，服务器会重传 SYN+ACK，客户端收到后会重传 ACK 并捎带重传数据 如果 ACK 没丢，服务器收到 ACK 后直接收下数据，一举两得 这就是 TCP 的“捎带确认”（Piggybacking）机制：利用第三次握手顺便传输数据，提高效率。\n📊 三次握手状态变迁图 步骤 操作 客户端状态 服务器状态 初始 无 CLOSED LISTEN ① 客户端发 SYN SYN_SENT LISTEN ② 服务器收到 SYN，回 SYN+ACK SYN_SENT SYN_RCVD ②→③ 客户端收到 SYN+ACK ESTABLISHED SYN_RCVD ③ 客户端发 ACK ESTABLISHED SYN_RCVD ③→ 服务器收到 ACK ESTABLISHED ESTABLISHED 💡 一句话总结三次握手 客户端先探路（SYN）→ 服务器回应并反问（SYN+ACK）→ 客户端最后确认（ACK），双方都确认“我能发、我能收、对方能发、对方能收”后，才开始正式通信。第三次 ACK 是给服务器的定心丸，确保服务器不会在“心里没底”的情况下发送数据。\nTCP 四次挥手 客户端 - 服务器\n四次挥手目的：客户端和服务器各自宣布“我没话说了”，并确认对方也“没话说了”，确保双方都完成数据收发后再断开连接，避免数据丢失。\n第一次挥手：客户端发 FIN（结束请求） 客户端：我的数据都发送完了，我不想再发数据了，但我还能收数据。 我发一个 FIN 消息给服务器，告诉服务器：“我这边没话说了，我要准备关闭我的发送通道了。” 如果没人回复，我再发几次（超时重传）。 这个时刻，客户端知道的事实是： 我发了 FIN，但不知道服务器收到没有 ❌ 服务器还有没有数据要发？不知道 ❌ 我还能收数据，但不知道还能收多久 ❌ 第二次挥手：服务器回 ACK（确认收到） 服务器收到了客户端的 FIN，知道客户端不再发数据了。\n服务器回复一个 ACK：“收到你的 FIN，我知道你没话说了。”\n这个时刻，服务器知道的事实是：\n客户端不再发新数据了 ✅ 服务器收消息是没问题的 ✅（收到了 FIN） 服务器还能继续发消息（因为服务器可能还有数据没发完）📦 客户端还能继续收消息 ✅（收到了 ACK 就说明客户端还在听） 服务器还没有发 FIN，因为数据没发完 当客户端收到 ACK 后，客户端可以确定以下事实：\n我发的 FIN 服务器收到了 ✅（我发消息没问题） 服务器知道我不再发数据了 ✅ 客户端不确定服务器还有没有数据要发 ❌ 客户端不确定服务器什么时候会关闭 ❌ 客户端还开着接收窗口，等着可能到来的数据 👂 进入**半关闭（Half-close）**状态：客户端不再发数据，但还能收数据 这个 ACK 的作用：只是告诉客户端“我知道你关了”，但服务器不会立即关闭自己的发送通道，因为可能还有数据要发给客户端。\n第三次挥手：服务器发 FIN（结束请求） 服务器：我的数据也全部发送完毕了，我也没话说了。\n服务器发一个 FIN 给客户端：“我的数据也发完了，我也要关闭我的发送通道了。”\n这个时刻，服务器知道的事实是：\n我发了 FIN，但不知道客户端收到没有 ❌ 我不知道客户端会不会给我回 ACK ❌ 如果客户端不回 ACK，我不知道该不该彻底关闭 ❌ 我需要等待客户端的 ACK 来确认我的 FIN 已送达 ✅ 当客户端收到 FIN 后，客户端可以确定以下事实：\n服务器发 FIN 了，说明服务器确实没话说了 ✅ 客户端之前担心“服务器还有没有数据”的顾虑消除了 ✅ 客户端需要回复一个 ACK，让服务器安心关闭 ✅ 但客户端还不能立即关闭，因为要等 ACK 送达确认 第四次挥手：客户端回 ACK（最终确认） 客户端收到服务器的 FIN，回复一个 ACK：“收到你的 FIN，确认关闭。”\n当服务器收到 ACK 后，服务器可以确定以下事实：\n我发的 FIN 客户端收到了 ✅ 客户端知道我要关闭了 ✅ 服务器可以安心关闭连接了 ✅ 服务器释放资源，进入 CLOSED 状态 🗑️ 但是客户端收到 FIN 后，不会立即关闭：\n客户端发完 ACK 后，进入 TIME_WAIT 状态 ⏳ 客户端需要等待 2MSL（最大报文生存时间，通常是 2 分钟） 为什么要等？ 如果最后一个 ACK 丢了，服务器会超时重传 FIN 客户端在 TIME_WAIT 期间如果收到服务器重传的 FIN，可以再发一次 ACK 确保服务器能收到最终的 ACK，避免服务器一直傻等 ❌ 保证网络中所有旧数据包都消失，不会干扰后续的新连接（同一个端口复用） 等待结束后，客户端才真正关闭连接，进入 CLOSED 状态 ⚠️ 如果第四次挥手的 ACK 丢了怎么办？ 客户端视角：我发了 ACK，进入 TIME_WAIT，等着服务器可能重传的 FIN。 服务器视角：我发了 FIN，怎么没收到 ACK？是不是丢了？ 服务器行为：超时后重传 FIN（有限次数）。 客户端在 TIME_WAIT 期间： 如果收到了服务器重传的 FIN → 我上次的 ACK 丢了 客户端再次发送 ACK，并重置 TIME_WAIT 计时器（重新开始 2MSL 倒计时） 如果服务器多次重传 FIN 都收不到 ACK： 服务器放弃等待，强制关闭连接（进入 CLOSED），释放资源 如果客户端 TIME_WAIT 结束还没收到重传的 FIN： 客户端认为服务器已经收到了 ACK，正常关闭，进入 CLOSED 这就是 TIME_WAIT 的意义：客户端在发完最后一个 ACK 后不能立即关闭，必须等一段时间，确保这个 ACK 万一丢了，还能重发，让服务器安心关闭。\n🤔 为什么挥手是四次，握手是三次？ 三次握手 四次挥手 能否合并 服务器的 SYN 和 ACK 可以合并成 SYN+ACK 服务器的 ACK 和 FIN 不能合并 为什么 服务器收到 SYN 时，不需要等待任何东西，立刻就知道“我也要建立连接”，所以可以把 SYN 和 ACK 合成一条发 服务器收到 FIN 时，可能还有数据没发完，不能立即关闭。必须先回 ACK 说“我知道了”，等数据全部发完后再单独发 FIN 本质区别 建立连接是双方同步，没有先后顺序 关闭连接是各自独立，谁先关谁后关取决于谁先发完数据 实际场景比喻：\n三次握手：两个人同时伸出手准备握手（SYN），同时回应对方（SYN+ACK），最后确认（ACK）。同步进行。 四次挥手：A 说“我说完了”（FIN），B 说“知道了”（ACK），但 B 还在继续说，等 B 说完了（FIN），A 说“知道了”（ACK）。各自独立。 📊 四次挥手状态变迁图 步骤 操作 客户端状态 服务器状态 初始 数据传输中 ESTABLISHED ESTABLISHED ① 客户端发 FIN FIN_WAIT_1 ESTABLISHED ② 服务器收到 FIN，回 ACK FIN_WAIT_2 CLOSE_WAIT ②→③ 客户端收到 ACK FIN_WAIT_2 CLOSE_WAIT ③ 服务器发 FIN（数据发完后） FIN_WAIT_2 LAST_ACK ③→④ 客户端收到 FIN TIME_WAIT LAST_ACK ④ 客户端发 ACK TIME_WAIT LAST_ACK ④→ 服务器收到 ACK TIME_WAIT CLOSED 2MSL 后 客户端等待结束 CLOSED CLOSED 💡 一句话总结四次挥手 客户端先关（发 FIN）→ 服务器回 ACK（同意，但还在发数据）→ 服务器后关（发 FIN）→ 客户端回 ACK（最终确认，并傻等一会儿）\n核心目的：\n确保双方都没话说了（数据全部送达） 确保最后一个 ACK 不会丢，避免服务器“悬着一颗心”不知道对方收没收到 确保网络中旧数据包消失，不影响后续新连接 半关闭（Half-close）机制：允许一方关闭发送通道后，继续接收另一方数据，直到另一方也关闭。 WebSocket 交互过程 1. 基础 WebSocket 交互流程（含握手） sequenceDiagram participant Client as 客户端 participant Server as WebSocket服务端 Note over Client,Server: 1. HTTP握手升级 Client-\u003e\u003eServer: HTTP GET /wsUpgrade: websocketSec-WebSocket-Key: xxx Server--\u003e\u003eClient: HTTP 101 Switching ProtocolsUpgrade: websocketSec-WebSocket-Accept: yyy Note over Client,Server: WebSocket连接已建立（全双工） Client-\u003e\u003eServer: 发送文本消息 Server--\u003e\u003eClient: 回复消息 Server-\u003e\u003eClient: 主动推送消息 Client--\u003e\u003eServer: 确认收到（业务层） Note over Client,Server: 2. 关闭连接 Client-\u003e\u003eServer: Close Frame (状态码: 1000) Server--\u003e\u003eClient: Close Frame (确认关闭) Note over Client,Server: 连接已关闭 2. 详细交互流程（包含心跳 Ping/Pong） sequenceDiagram participant Client as 客户端 participant Server as WebSocket服务端 participant App as 业务应用层 Note over Client,App: 阶段1: 握手建立 Client-\u003e\u003eServer: HTTP请求（Upgrade: websocket） Server--\u003e\u003eClient: 101 Switching Protocols Server-\u003e\u003eApp: 触发 @OnOpen 事件 Note over App: 保存Session到内存 Note over Client,App: 阶段2: 双向通信 Client-\u003e\u003eServer: 发送业务消息（如：查询订单） Server-\u003e\u003eApp: 触发 @OnMessage App--\u003e\u003eServer: 处理业务逻辑 Server--\u003e\u003eClient: 推送响应数据 Server-\u003e\u003eClient: 主动推送（如：系统通知） Client--\u003e\u003eServer: 收到确认（业务层ACK） Note over Client,App: 阶段3: 心跳保活（WebSocket协议层） loop 每30秒 Server-\u003e\u003eClient: Ping Frame (协议层) Client--\u003e\u003eServer: Pong Frame (协议层) Note over Server: 更新最后心跳时间 end Note over Client,App: 阶段4: 异常断开或主动关闭 alt 主动关闭 Client-\u003e\u003eServer: Close Frame (1000 Normal) Server--\u003e\u003eClient: Close Frame (确认) Server-\u003e\u003eApp: 触发 @OnClose else 异常断开（网络超时） Note over Server: 心跳超时检测到 Server-\u003e\u003eApp: 触发 @OnError Server-\u003e\u003eApp: 触发 @OnClose end Note over App: 清理Session引用 3. 分布式场景下的 WebSocket（多节点 + Redis） sequenceDiagram participant Client1 as 客户端1(连接Node-A) participant Client2 as 客户端2(连接Node-B) participant NodeA as Node-A(WebSocket服务器) participant NodeB as Node-B(WebSocket服务器) participant Redis as Redis Pub/Sub Note over Client1,NodeA: 客户端1连接到Node-A Client1-\u003e\u003eNodeA: WebSocket握手 NodeA--\u003e\u003eClient1: 连接建立 Note over NodeA: 保存Session1到本地内存 Note over Client2,NodeB: 客户端2连接到Node-B Client2-\u003e\u003eNodeB: WebSocket握手 NodeB--\u003e\u003eClient2: 连接建立 Note over NodeB: 保存Session2到本地内存 Note over Client1,Redis: 场景：Client1给Client2发消息 Client1-\u003e\u003eNodeA: 发送消息（目标: Client2） NodeA-\u003e\u003eRedis: 发布消息到频道(目标用户: Client2) Redis-\u003e\u003eNodeB: 广播消息到所有订阅节点 NodeB-\u003e\u003eNodeB: 查找本地Session2 NodeB--\u003e\u003eClient2: 推送消息到Client2 Note over Client1,Client2: 转发完成 4. 完整的生命周期状态图 stateDiagram-v2 [*] --\u003e 连接中: 发起HTTP请求(Upgrade: websocket) 连接中 --\u003e 已连接: 收到101响应(握手成功) 连接中 --\u003e 握手失败: 非101响应或超时 已连接 --\u003e 消息交互: 发送/接收消息 消息交互 --\u003e 已连接: 持续通信 已连接 --\u003e 心跳检测: 定期发送Ping 心跳检测 --\u003e 已连接: 收到Pong响应 心跳检测 --\u003e 超时断开: 多次Ping无响应 已连接 --\u003e 主动关闭: 发送Close Frame 主动关闭 --\u003e 已关闭: 收到Close确认 超时断开 --\u003e 已关闭: 触发OnClose 握手失败 --\u003e [*] 已关闭 --\u003e [*] note right of 已连接 Session可用 isOpen() == true end note note right of 已关闭 Session失效 isOpen() == false 清理内存引用 end note 5. WebSocket 帧结构交互（协议层细节） sequenceDiagram participant Client as 客户端 participant Server as 服务端 Note over Client,Server: 数据帧交互（RFC 6455） Client-\u003e\u003eServer: 文本帧 (Opcode=0x1)FIN=1, Payload=\"Hello\" Server--\u003e\u003eClient: 文本帧 (Opcode=0x1)FIN=1, Payload=\"World\" Note over Client,Server: 分片传输（大消息） Client-\u003e\u003eServer: 文本帧 (Opcode=0x1, FIN=0)片段1 Client-\u003e\u003eServer: 连续帧 (Opcode=0x0, FIN=0)片段2 Client-\u003e\u003eServer: 结束帧 (Opcode=0x0, FIN=1)片段3 Note over Client,Server: 控制帧（心跳） Server-\u003e\u003eClient: Ping帧 (Opcode=0x9) Client--\u003e\u003eServer: Pong帧 (Opcode=0xA) Note over Client,Server: 关闭连接 Client-\u003e\u003eServer: 关闭帧 (Opcode=0x8)状态码=1000, Reason=\"Bye\" Server--\u003e\u003eClient: 关闭帧 (Opcode=0x8)状态码=1000 6. 异常处理的交互流程 sequenceDiagram participant Client as 客户端 participant Server as 服务端 participant Session as Session对象 participant Cleaner as 清理器 Note over Client,Cleaner: 异常场景处理 rect Note over Client,Server: 场景1: 网络突然断开 Client--xServer: 网络中断（未发送Close） Note over Server: 心跳超时检测（60秒） Server-\u003e\u003eSession: 检测到超时 Session-\u003e\u003eCleaner: 触发@OnError Cleaner-\u003e\u003eCleaner: 移除本地Session引用 Cleaner-\u003e\u003eCleaner: 关闭资源（线程池等） Note over Cleaner: Session.isOpen() == false end rect Note over Client,Server: 场景2: 业务异常 Client-\u003e\u003eServer: 发送非法消息 Server-\u003e\u003eServer: 解析异常 Server-\u003e\u003eSession: 触发@OnError Session--\u003e\u003eClient: 发送关闭帧（状态码: 1002） Session-\u003e\u003eCleaner: 触发@OnClose Cleaner-\u003e\u003eCleaner: 清理资源 end 7. 总结：WebSocket Session 在流程中的位置 graph TB subgraph \"连接建立阶段\" A[HTTP握手] --\u003e B[创建Session实例] B --\u003e C[触发OnOpen] C --\u003e D[保存到内存Map] end subgraph \"通信阶段\" E[接收消息] --\u003e F[通过Session获取连接] F --\u003e G[触发OnMessage] G --\u003e H[业务处理] H --\u003e I[通过Session.send回复] end subgraph \"连接关闭阶段\" J[收到Close帧或超时] --\u003e K[触发OnClose] K --\u003e L[从Map移除Session] L --\u003e M[关闭底层Channel] M --\u003e N[Session失效] end D --\u003e E I --\u003e J Spring 中编写代码的流程 @Configuration WebSocketConfig addEndpoint - 接收来自哪里的URL setApplicationDestinationPrefixes - 接收来自哪个路径的消息 enableSimpleBroker - 开启哪些路径的广播 JwtHandshakeInterceptor - handshake时验证用户的token，放ws的attributes CustomHandshakeHandler extends DefaultHandshakeHandler - handshake时将attributes中的用户信息存入 Principal @Component WebSocketEventListener - 处理连接、订阅、取消订阅、失去连接时的行为 注意失去连接不会自动取消所有订阅！ @ControllerAdvice WebSocketExceptionHandleController - 处理异常 @MessageExceptionHandler(Exception.class) 指定哪些Exception @SendToUser(\u0026quot;/queue/errors\u0026quot;) 指定错误消息广播给哪个路径，注意spring会加上 \u0026ldquo;/user\u0026rdquo; @Controller ChatController - 接收到消息如何处理 @MessageMapping(\u0026quot;/chat.send\u0026quot;) - 不用写前缀，setApplicationDestinationPrefixes已有默认前缀 MQ 基于 WebSocket 的微服务聊天处理流程 一、核心设计原则 先入库，再分发 —— 消息可靠性的底线 离线判定只做一次 —— 在发布端判定，避免 N 个实例重复推送 本地内存是唯一的 Session 真相源 —— Redis 只是\u0026quot;路由表\u0026quot;，不存 Session Redis 在线状态可能撒谎 —— 必须容忍脏数据（宕机残留），靠 TTL + 心跳兜底 二、连接建立阶段 1. 用户 A 发起 WS 连接 ws://gateway/chat?token=xxx 走 Gateway，握手阶段（HTTP Upgrade）就完成鉴权 鉴权失败直接拒绝握手，不要建立连接后再踢 2. 建立连接通道 // 握手拦截器 public boolean beforeHandshake(...) { String token = extractToken(request); Claims claims = jwtUtil.verify(token); // RSA 验签 attributes.put(\u0026#34;userId\u0026#34;, claims.getSubject()); return true; } 3. 记录本地在线状态（JVM 内存） // key: userId, value: WebSocketSession ConcurrentHashMap\u0026lt;Long, WebSocketSession\u0026gt; LOCAL_SESSIONS; 只有本实例能操作自己的 Session，这是不可跨进程的资源。\n4. 记录 Redis 全局在线状态 HSET online:user:{userId} instanceId \u0026#34;chat-svc-192.168.1.10:8081\u0026#34; EXPIRE online:user:{userId} 90s ← 关键！必须有 TTL 为什么必须有 TTL：\n实例被 kill -9 时来不及清理，Redis 会残留\u0026quot;僵尸在线\u0026quot; 客户端会周期性发心跳（如 30s），服务端收到后 EXPIRE 续期 90s 内没心跳 → 自动过期 → 视为离线 → 走推送 多端登录场景： 用 Hash 存 deviceId -\u0026gt; instanceId，一个用户可对应多个实例。\n三、消息发送阶段（核心链路） 1. 客户端发消息 { \u0026#34;msgId\u0026#34;: \u0026#34;客户端生成的UUID\u0026#34;, // 幂等去重用 \u0026#34;conversationId\u0026#34;: 12345, // 会话/群组ID \u0026#34;content\u0026#34;: \u0026#34;hello\u0026#34;, \u0026#34;type\u0026#34;: \u0026#34;TEXT\u0026#34; } 2. 服务端接收 → 持久化（第一优先级） Message msg = buildMessage(dto); msg.setSeq(seqGenerator.next(conversationId)); // 会话内递增序号 messageMapper.insert(msg); // 必须先入库！ 为什么必须先入库：\n入库成功 = 消息已被系统接受，后续所有环节失败都可以补救（客户端拉历史） 若先推送后入库，推送成功但入库失败 → 消息永久丢失，且不同用户看到的历史不一致 这里要加：\nmsgId 唯一索引 → 客户端重试时天然幂等 seq 会话内单调递增 → 客户端可据此发现空洞并主动补拉 3. 查询群成员 + 在发布端完成离线判定 // Step 1: 取群成员（走 Redis 缓存，miss 再查 DB） List\u0026lt;Long\u0026gt; members = groupService.getMembers(conversationId); // Step 2: 批量查 Redis 在线状态（用 Pipeline，不要 N 次 RTT） Map\u0026lt;Long, String\u0026gt; onlineMap = redisTemplate.executePipelined(...); // Step 3: 分流 List\u0026lt;Long\u0026gt; onlineUsers = members ∩ onlineMap.keySet(); List\u0026lt;Long\u0026gt; offlineUsers = members - onlineMap.keySet(); // Step 4: 离线的 → 直接发 Kafka（此刻就决定，只发一次） if (!offlineUsers.isEmpty()) { kafkaTemplate.send(\u0026#34;push-topic\u0026#34;, new PushEvent(msg, offlineUsers)); } // Step 5: 在线的 → Publish 到 Redis Channel redisTemplate.convertAndSend(\u0026#34;chat:broadcast\u0026#34;, new BroadcastEvent(msg, onlineUsers)); 为什么离线判定必须在这里做？ 如果放到订阅端做，3 个实例订阅同一个 Channel，每个实例都会发现\u0026quot;用户 B 不在线\u0026quot; → 发 3 条 Kafka → 用户收到 3 条重复推送。\n发布端只有 1 个，判定天然只做一次。\n四、消息投递阶段（订阅端） 每个实例都订阅 chat:broadcast，收到后：\npublic void onMessage(BroadcastEvent event) { // 只做一件事：找自己内存里有的 Session for (Long uid : event.getTargetUsers()) { WebSocketSession session = LOCAL_SESSIONS.get(uid); if (session != null \u0026amp;\u0026amp; session.isOpen()) { session.sendMessage(msg); // 命中，发送 } // 不在本地 → 直接忽略，交给别的实例 } } 四种情况对照表：\nRedis 状态 本地内存 处理方式 在线 有 Session ✅ 直接推送 在线 无 Session 🔇 忽略（别的实例会处理） 离线 有 Session ⚠️ 脏数据！Redis 过期但连接还在 → 补写 Redis 并推送 离线 无 Session 📮 已在发布端走 Kafka，此处不重复处理 兜底做法：本地有 Session 就发，同时 EXPIRE 续期。用户可能收到「WS 消息 + 推送」两份，客户端用 msgId 去重即可。宁可重复，不可丢失。\n五、离线推送阶段（Kafka 消费者） 消费流程 @KafkaListener(topics = \u0026#34;push-topic\u0026#34;) public void handle(PushEvent event, Acknowledgment ack) { try { // 二次确认：Kafka 有延迟，用户可能已经上线了 if (redisTemplate.hasKey(\u0026#34;online:user:\u0026#34; + uid)) { return; // 已上线，WS 会送达，跳过推送 } pushClient.send(uid, event.getMsg()); // APNs / FCM / 厂商通道 ack.acknowledge(); } catch (Exception e) { throw e; // 交给错误处理器 } } 失败处理三级火箭 @Bean public DefaultErrorHandler errorHandler(KafkaTemplate\u0026lt;?,?\u0026gt; template) { // 1. 重试：指数退避，避免打垮下游 ExponentialBackOff backOff = new ExponentialBackOff(1000L, 2.0); backOff.setMaxElapsedTime(30_000L); // 最多重试 30s // 2. 重试耗尽 → 进死信队列 DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(template, (r, e) -\u0026gt; new TopicPartition(\u0026#34;push-topic.DLT\u0026#34;, r.partition())); DefaultErrorHandler handler = new DefaultErrorHandler(recoverer, backOff); // 3. 不可重试异常直接进 DLT（如参数错误、用户不存在） handler.addNotRetryableExceptions(IllegalArgumentException.class); return handler; } 分层策略：\n异常类型 处理 网络抖动、下游 5xx 重试（指数退避） Token 失效、用户注销 不重试，直接 DLT 重试耗尽 DLT + 告警 DLT 堆积 监控指标 → 报警 → 人工介入 DLT 的运维闭环：\nDLT 消息保留 7 天 Prometheus 监控 kafka_consumergroup_lag{topic=\u0026quot;push-topic.DLT\u0026quot;} 提供管理后台，支持查看 + 手动重投 补充：推送失败其实不算致命。消息已经在库里了，用户下次打开 App 主动拉取即可看到。推送只是\u0026quot;提醒\u0026quot;，不是\u0026quot;投递\u0026quot;。这个认知能让你的降级策略轻松很多。\n六、连接断开阶段 正常断开（onClose） public void afterConnectionClosed(WebSocketSession session, CloseStatus status) { Long uid = getUserId(session); LOCAL_SESSIONS.remove(uid); // 关键：CAS 删除，只删自己写的那条 // 防止用户已在别的实例重连，把新记录误删了 String owner = redis.opsForHash().get(\u0026#34;online:user:\u0026#34;+uid, deviceId); if (INSTANCE_ID.equals(owner)) { redis.opsForHash().delete(\u0026#34;online:user:\u0026#34;+uid, deviceId); } } CAS 校验\n用户从 4G 切 WiFi → 在实例 B 重连成功 → 实例 A 才收到断开事件 → 若无脑 DEL，会把实例 B 刚写的记录删掉 → 用户明明在线却被判离线。\n异常断开（进程被杀） 依赖 Redis TTL 自动过期（90s） 实例启动时可扫描并清理属于自己 instanceId 的残留 key 心跳保活 客户端 --ping--\u0026gt; 服务端（每 30s） 服务端 --pong--\u0026gt; 客户端 + EXPIRE online:user:{uid} 90s IdleTimeout 设 60s，超时未收到 ping 主动关闭连接。\n七、完整流程图 flowchart TD A[用户A: WS握手请求] --\u003e B{Gateway 鉴权} B --\u003e|失败| B1[拒绝握手] B --\u003e|成功| C[建立 WS 连接] C --\u003e D[写入 JVM 本地ConcurrentHashMap] D --\u003e E[\"写入 Redisonline:user:uid → instanceIdTTL 90s\"] E --\u003e F[心跳循环每30s续期TTL] F --\u003e G[用户A 发送消息] G --\u003e H[\"1️⃣ 消息入库 (必须最先)msgId唯一索引 + seq递增\"] H --\u003e I[查询群成员列表] I --\u003e J[\"Pipeline 批量查Redis 在线状态\"] J --\u003e K{发布端分流判定只做一次} K --\u003e|离线成员| L[\"Kafka → push-topic\"] K --\u003e|在线成员| M[\"Redis Publishchat:broadcast\"] L --\u003e L1{推送服务消费} L1 --\u003e L2{二次确认是否已上线?} L2 --\u003e|已上线| L3[跳过，WS会送达] L2 --\u003e|仍离线| L4[调用 APNs/FCM] L4 --\u003e|成功| L5[ACK 提交位点] L4 --\u003e|失败| L6{可重试异常?} L6 --\u003e|是| L7[指数退避重试1s→2s→4s...最长30s] L6 --\u003e|否| L8[直接进 DLT] L7 --\u003e|重试耗尽| L8 L8 --\u003e L9[DLT告警 → 管理后台人工介入/手动重投] L3 --\u003e L10[消息已入库客户端可主动拉取] M --\u003e N[所有实例订阅收到] N --\u003e O{查本地 LOCAL_SESSIONS} O --\u003e|命中且Open| P[✅ session.send 推送] O --\u003e|未命中| Q[🔇 忽略交给持有Session的实例] O --\u003e|命中但Redis已过期| R[\"⚠️ 脏数据兜底补写Redis + 推送\"] P --\u003e S[客户端按 msgId 去重] R --\u003e S S --\u003e T[用户断开连接] T --\u003e U{断开类型} U --\u003e|正常 onClose| V[移除本地 Session] V --\u003e W{\"CAS校验owner == 本实例?\"} W --\u003e|是| X[删除 Redis 记录] W --\u003e|否| Y[跳过用户已在别处重连] U --\u003e|进程被kill| Z[Redis TTL 90s自动过期兜底] style H fill:#ff6b6b,color:#fff style K fill:#ff6b6b,color:#fff style W fill:#ffa94d,color:#000 style R fill:#ffa94d,color:#000 ","permalink":"https://lv-blog.pages.dev/posts/programming/backend/fundamentals-for-building-a-chatroom/","summary":"\u003ch1 id=\"基础知识\"\u003e基础知识\u003c/h1\u003e\n\u003ch2 id=\"tcp-三次握手\"\u003eTCP 三次握手\u003c/h2\u003e\n\u003cp\u003e客户端 - 服务器\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e三次握手目的\u003c/strong\u003e：客户端的收发没问题，服务器的收发也没问题。\u003c/p\u003e\n\u003chr\u003e\n\u003ch4 id=\"第一次握手客户端发-syn同步请求\"\u003e第一次握手：客户端发 SYN（同步请求）\u003c/h4\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e客户端\u003c/strong\u003e：我啥也不知道，我发个 \u003cstrong\u003eSYN\u003c/strong\u003e 消息出去，看看有没有人接收到。\u003c/li\u003e\n\u003cli\u003e如果没人回复，我再发几次（超时重传）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e这个时刻，客户端知道的事实是\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003e我发出去了，但不知道有没有人收到 ❌\u003c/li\u003e\n\u003cli\u003e我能不能收消息？不知道 ❌\u003c/li\u003e\n\u003cli\u003e对方存不存在？不知道 ❌\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr\u003e\n\u003ch4 id=\"第二次握手服务器回-synack同步确认\"\u003e第二次握手：服务器回 SYN+ACK（同步+确认）\u003c/h4\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e服务器\u003c/strong\u003e收到了客户端的 SYN。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e服务器回复 \u003cstrong\u003eSYN+ACK\u003c/strong\u003e：“我收到你的消息了，我也发一条试试，看看你能不能收到。”\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e这个时刻，服务器知道的事实是\u003c/strong\u003e：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e客户端发消息是没有问题的 ✅（因为我收到了）\u003c/li\u003e\n\u003cli\u003e服务器收消息是没有问题的 ✅（因为我收到了）\u003c/li\u003e\n\u003cli\u003e服务器不知道服务器发消息有没有问题 ❌（我发出去了，但不知道对方收到没）\u003c/li\u003e\n\u003cli\u003e服务器不知道客户端收消息有没有问题 ❌（我发出去了，但不知道对方收到没）\u003c/li\u003e\n\u003cli\u003e服务器需要发送一条 SYN+ACK，等对方回复 ACK 来确认“我能发、对方能收”\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e当客户端收到 SYN+ACK 后，客户端可以确定以下事实\u003c/strong\u003e：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e我发消息是没有问题的 ✅（我发的 SYN 被收到了）\u003c/li\u003e\n\u003cli\u003e我收消息也是没有问题的 ✅（我收到了对方的回复）\u003c/li\u003e\n\u003cli\u003e服务器收消息是没有问题的 ✅（对方收到了我的 SYN）\u003c/li\u003e\n\u003cli\u003e服务器发消息也是没有问题的 ✅（我收到了对方的 SYN+ACK）\u003c/li\u003e\n\u003cli\u003e✅ \u003cstrong\u003e客户端这边已经确认全双工通信没问题了！\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e但是服务器他还有顾虑（不知道自己能不能发、不知道我能不能收），我再发个 ACK 给他吧\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr\u003e\n\u003ch4 id=\"第三次握手客户端发-ack确认\"\u003e第三次握手：客户端发 ACK（确认）\u003c/h4\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e客户端\u003c/strong\u003e收到 SYN+ACK 后，回复一个 \u003cstrong\u003eACK\u003c/strong\u003e：“我收到你的 SYN+ACK 了，我这边收发都没问题，你也可以放心了。”\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e当服务器收到这个 ACK 后，服务器可以确定以下事实\u003c/strong\u003e：\u003c/p\u003e","title":"构建一个聊天室需要的基础知识"},{"content":"正常 JWT 请求流程 sequenceDiagram participant User as 用户 participant Frontend as 前端（浏览器/App） participant Auth as 认证服务器 participant Backend as 后端API服务器 Note over User,Backend: 1. 登录阶段 User-\u003e\u003eFrontend: 输入用户名/密码 Frontend-\u003e\u003eAuth: POST /login (凭证) Auth-\u003e\u003eAuth: 验证凭证 Auth--\u003e\u003eFrontend: 返回 access_token + refresh_token Frontend-\u003e\u003eFrontend: 存储token（内存/localStorage） Frontend--\u003e\u003eUser: 登录成功 Note over User,Backend: 2. 正常请求阶段 User-\u003e\u003eFrontend: 请求受保护资源 Frontend-\u003e\u003eBackend: GET /api/resourceAuthorization: Bearer access_token Backend-\u003e\u003eBackend: 验证access_token签名和过期时间 Backend--\u003e\u003eFrontend: 返回请求的资源 Frontend--\u003e\u003eUser: 展示数据 Note over User,Backend: 3. Access Token过期 User-\u003e\u003eFrontend: 继续请求 Frontend-\u003e\u003eBackend: GET /api/resourceAuthorization: Bearer access_token(已过期) Backend--\u003e\u003eFrontend: 401 Unauthorized (token过期) Frontend-\u003e\u003eFrontend: 检测到401，触发刷新逻辑 Note over Frontend,Auth: 4. 刷新Token阶段 Frontend-\u003e\u003eAuth: POST /refreshrefresh_token Auth-\u003e\u003eAuth: 验证refresh_token Auth--\u003e\u003eFrontend: 返回新的 access_token（可选的新refresh_token） Frontend-\u003e\u003eFrontend: 更新存储的access_token Frontend-\u003e\u003eBackend: 重试原请求（新access_token） Backend--\u003e\u003eFrontend: 返回请求的资源 Frontend--\u003e\u003eUser: 展示数据 为什么不能只靠JWT refresh token 发出去后后端不保存，会变成 不可控的长期通行证 无法主动踢人下线 无法注销单点设备 无法判断 token 是否被盗用（攻击者拿到了 refresh token 可以一直刷新 access token） 无法实现会话管理 使用 refresh tokens 表后的流程图 sequenceDiagram participant User as 用户 participant Frontend as 前端 participant Auth as 认证服务器 participant DB as 数据库(refresh_tokens表) participant Backend as 后端API服务器 Note over User,DB: 1. 登录阶段（签发token + 入库） User-\u003e\u003eFrontend: 输入用户名/密码 Frontend-\u003e\u003eAuth: POST /login Auth-\u003e\u003eAuth: 验证凭证 Auth-\u003e\u003eDB: 生成唯一refresh_token_idINSERT INTO refresh_tokens(user_id, token_hash, expires_at, device_info, ip_address, revoked) DB--\u003e\u003eAuth: 插入成功 Auth-\u003e\u003eAuth: 生成access_token + refresh_token(refresh_token包含id引用) Auth--\u003e\u003eFrontend: 返回access_token + refresh_token Frontend-\u003e\u003eFrontend: 存储token Frontend--\u003e\u003eUser: 登录成功 Note over User,Backend: 2. 正常请求（与之前相同） User-\u003e\u003eFrontend: 请求资源 Frontend-\u003e\u003eBackend: GET /api/resourceAuthorization: Bearer access_token Backend-\u003e\u003eBackend: 验证access_token Backend--\u003e\u003eFrontend: 返回资源 Note over Frontend,DB: 3. Access Token过期 → 刷新 Frontend-\u003e\u003eBackend: GET /api/resource (access_token过期) Backend--\u003e\u003eFrontend: 401 Unauthorized Frontend-\u003e\u003eAuth: POST /refreshrefresh_token Note over Auth,DB: 4. 刷新验证（多步校验） Auth-\u003e\u003eAuth: 解析refresh_token，提取token_id Auth-\u003e\u003eDB: SELECT * FROM refresh_tokensWHERE id = token_id DB--\u003e\u003eAuth: 返回记录 Auth-\u003e\u003eAuth: 校验清单： Note over Auth: ✅ token_hash是否匹配✅ 是否过期 (expires_at \u003e now())✅ 是否被撤销 (revoked = false)✅ 用户是否仍有效✅ 设备信息是否一致（可选） alt 校验全部通过 Auth-\u003e\u003eDB: UPDATE refresh_tokensSET last_used_at = now(), last_used_ip = current_ipWHERE id = token_id Auth-\u003e\u003eAuth: 生成新access_token（可选：延长refresh_token有效期） Auth--\u003e\u003eFrontend: 返回新access_token Frontend-\u003e\u003eFrontend: 更新access_token Frontend-\u003e\u003eBackend: 重试原请求（新token） Backend--\u003e\u003eFrontend: 返回资源 else 校验失败 Auth--\u003e\u003eFrontend: 401/403 (刷新失败) Frontend-\u003e\u003eFrontend: 清除所有token Frontend-\u003e\u003eUser: 跳转登录页 end Note over User,DB: 5. 主动登出 User-\u003e\u003eFrontend: 点击登出 Frontend-\u003e\u003eAuth: POST /logoutrefresh_token Auth-\u003e\u003eDB: UPDATE refresh_tokensSET revoked = trueWHERE id = token_id Auth--\u003e\u003eFrontend: 登出成功 Frontend-\u003e\u003eFrontend: 清除本地token Frontend--\u003e\u003eUser: 已登出 Note over User,DB: 6. 安全场景：密码修改 User-\u003e\u003eFrontend: 修改密码 Frontend-\u003e\u003eAuth: POST /change-password Auth-\u003e\u003eDB: UPDATE refresh_tokensSET revoked = trueWHERE user_id = current_user_id Note over DB: 撤销该用户所有refresh_token（强制所有设备重新登录） Auth--\u003e\u003eFrontend: 密码修改成功 Frontend-\u003e\u003eFrontend: 清除本地token Frontend--\u003e\u003eUser: 请重新登录 ","permalink":"https://lv-blog.pages.dev/posts/programming/backend/why-backend-maintains-a-refresh-tokens-table/","summary":"\u003ch1 id=\"正常-jwt-请求流程\"\u003e正常 JWT 请求流程\u003c/h1\u003e\n\u003cpre class=\"mermaid\"\u003esequenceDiagram\n    participant User as 用户\n    participant Frontend as 前端（浏览器/App）\n    participant Auth as 认证服务器\n    participant Backend as 后端API服务器\n\n    Note over User,Backend: 1. 登录阶段\n    User-\u003e\u003eFrontend: 输入用户名/密码\n    Frontend-\u003e\u003eAuth: POST /login (凭证)\n    Auth-\u003e\u003eAuth: 验证凭证\n    Auth--\u003e\u003eFrontend: 返回 access_token + refresh_token\n    Frontend-\u003e\u003eFrontend: 存储token（内存/localStorage）\n    Frontend--\u003e\u003eUser: 登录成功\n\n    Note over User,Backend: 2. 正常请求阶段\n    User-\u003e\u003eFrontend: 请求受保护资源\n    Frontend-\u003e\u003eBackend: GET /api/resource\u003cbr/\u003eAuthorization: Bearer access_token\n    Backend-\u003e\u003eBackend: 验证access_token签名和过期时间\n    Backend--\u003e\u003eFrontend: 返回请求的资源\n    Frontend--\u003e\u003eUser: 展示数据\n\n    Note over User,Backend: 3. Access Token过期\n    User-\u003e\u003eFrontend: 继续请求\n    Frontend-\u003e\u003eBackend: GET /api/resource\u003cbr/\u003eAuthorization: Bearer access_token(已过期)\n    Backend--\u003e\u003eFrontend: 401 Unauthorized (token过期)\n    Frontend-\u003e\u003eFrontend: 检测到401，触发刷新逻辑\n\n    Note over Frontend,Auth: 4. 刷新Token阶段\n    Frontend-\u003e\u003eAuth: POST /refresh\u003cbr/\u003erefresh_token\n    Auth-\u003e\u003eAuth: 验证refresh_token\n    Auth--\u003e\u003eFrontend: 返回新的 access_token（可选的新refresh_token）\n    Frontend-\u003e\u003eFrontend: 更新存储的access_token\n    Frontend-\u003e\u003eBackend: 重试原请求（新access_token）\n    Backend--\u003e\u003eFrontend: 返回请求的资源\n    Frontend--\u003e\u003eUser: 展示数据\n\u003c/pre\u003e\n\u003ch1 id=\"为什么不能只靠jwt\"\u003e为什么不能只靠JWT\u003c/h1\u003e\n\u003col\u003e\n\u003cli\u003erefresh token 发出去后后端不保存，会变成 \u003cstrong\u003e不可控的长期通行证\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e无法主动踢人下线\u003c/li\u003e\n\u003cli\u003e无法注销单点设备\u003c/li\u003e\n\u003cli\u003e无法判断 token 是否被盗用（攻击者拿到了 refresh token 可以一直刷新 access token）\u003c/li\u003e\n\u003cli\u003e无法实现会话管理\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch1 id=\"使用-refresh-tokens-表后的流程图\"\u003e使用 refresh tokens 表后的流程图\u003c/h1\u003e\n\u003cpre class=\"mermaid\"\u003esequenceDiagram\n    participant User as 用户\n    participant Frontend as 前端\n    participant Auth as 认证服务器\n    participant DB as 数据库(refresh_tokens表)\n    participant Backend as 后端API服务器\n\n    Note over User,DB: 1. 登录阶段（签发token + 入库）\n    User-\u003e\u003eFrontend: 输入用户名/密码\n    Frontend-\u003e\u003eAuth: POST /login\n    Auth-\u003e\u003eAuth: 验证凭证\n    Auth-\u003e\u003eDB: 生成唯一refresh_token_id\u003cbr/\u003eINSERT INTO refresh_tokens\u003cbr/\u003e(user_id, token_hash, expires_at,\u003cbr/\u003e device_info, ip_address, revoked)\n    DB--\u003e\u003eAuth: 插入成功\n    Auth-\u003e\u003eAuth: 生成access_token + refresh_token\u003cbr/\u003e(refresh_token包含id引用)\n    Auth--\u003e\u003eFrontend: 返回access_token + refresh_token\n    Frontend-\u003e\u003eFrontend: 存储token\n    Frontend--\u003e\u003eUser: 登录成功\n\n    Note over User,Backend: 2. 正常请求（与之前相同）\n    User-\u003e\u003eFrontend: 请求资源\n    Frontend-\u003e\u003eBackend: GET /api/resource\u003cbr/\u003eAuthorization: Bearer access_token\n    Backend-\u003e\u003eBackend: 验证access_token\n    Backend--\u003e\u003eFrontend: 返回资源\n\n    Note over Frontend,DB: 3. Access Token过期 → 刷新\n    Frontend-\u003e\u003eBackend: GET /api/resource (access_token过期)\n    Backend--\u003e\u003eFrontend: 401 Unauthorized\n    Frontend-\u003e\u003eAuth: POST /refresh\u003cbr/\u003erefresh_token\n\n    Note over Auth,DB: 4. 刷新验证（多步校验）\n    Auth-\u003e\u003eAuth: 解析refresh_token，提取token_id\n    Auth-\u003e\u003eDB: SELECT * FROM refresh_tokens\u003cbr/\u003eWHERE id = token_id\n    DB--\u003e\u003eAuth: 返回记录\n    \n    Auth-\u003e\u003eAuth: 校验清单：\n    Note over Auth: ✅ token_hash是否匹配\u003cbr/\u003e✅ 是否过期 (expires_at \u003e now())\u003cbr/\u003e✅ 是否被撤销 (revoked = false)\u003cbr/\u003e✅ 用户是否仍有效\u003cbr/\u003e✅ 设备信息是否一致（可选）\n    \n    alt 校验全部通过\n        Auth-\u003e\u003eDB: UPDATE refresh_tokens\u003cbr/\u003eSET last_used_at = now(),\u003cbr/\u003e    last_used_ip = current_ip\u003cbr/\u003eWHERE id = token_id\n        Auth-\u003e\u003eAuth: 生成新access_token\u003cbr/\u003e（可选：延长refresh_token有效期）\n        Auth--\u003e\u003eFrontend: 返回新access_token\n        Frontend-\u003e\u003eFrontend: 更新access_token\n        Frontend-\u003e\u003eBackend: 重试原请求（新token）\n        Backend--\u003e\u003eFrontend: 返回资源\n    else 校验失败\n        Auth--\u003e\u003eFrontend: 401/403 (刷新失败)\n        Frontend-\u003e\u003eFrontend: 清除所有token\n        Frontend-\u003e\u003eUser: 跳转登录页\n    end\n\n    Note over User,DB: 5. 主动登出\n    User-\u003e\u003eFrontend: 点击登出\n    Frontend-\u003e\u003eAuth: POST /logout\u003cbr/\u003erefresh_token\n    Auth-\u003e\u003eDB: UPDATE refresh_tokens\u003cbr/\u003eSET revoked = true\u003cbr/\u003eWHERE id = token_id\n    Auth--\u003e\u003eFrontend: 登出成功\n    Frontend-\u003e\u003eFrontend: 清除本地token\n    Frontend--\u003e\u003eUser: 已登出\n\n    Note over User,DB: 6. 安全场景：密码修改\n    User-\u003e\u003eFrontend: 修改密码\n    Frontend-\u003e\u003eAuth: POST /change-password\n    Auth-\u003e\u003eDB: UPDATE refresh_tokens\u003cbr/\u003eSET revoked = true\u003cbr/\u003eWHERE user_id = current_user_id\n    Note over DB: 撤销该用户所有refresh_token\u003cbr/\u003e（强制所有设备重新登录）\n    Auth--\u003e\u003eFrontend: 密码修改成功\n    Frontend-\u003e\u003eFrontend: 清除本地token\n    Frontend--\u003e\u003eUser: 请重新登录\n\u003c/pre\u003e","title":"为什么有时候后端需要维护一张Refresh_Tokens表"},{"content":" 数据来源：Spring Cloud Alibaba 官方 Wiki\nSpringBoot 3.3.4 之后 Spring Cloud Alibaba Version Spring Cloud Version Spring Boot Version 2025.1.0.0 2025.1.0 4.0.0 Spring Cloud Alibaba Version Spring Cloud Version Spring Boot Version 2025.0.0.0 2025.0.0 3.5.0 组件版本关系 每个 Spring Cloud Alibaba 版本及其自身所适配的各组件对应版本如下表所示：\nSpring Cloud Alibaba Version Sentinel Version Nacos Version RocketMQ Version SchedulerX Version Seata Version 2025.1.0.0 1.8.9 3.1.1 5.3.1 1.13.3 2.5.0 2025.0.0.0 1.8.9 3.0.3 5.3.1 1.13.1 2.5.0 依赖引入 \u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- Spring Boot 版本统一管理 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.0\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spring Cloud 官方版本管理 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2025.0.0\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- Spring Cloud Alibaba 阿里生态统一版本 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-alibaba-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2025.0.0.0\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; SpringBoot 3.3.4 及以前 📌 新版本（Calendar Versioning，推荐使用） Spring Cloud Alibaba Spring Cloud Spring Boot 2023.0.3.2 2023.0.3 3.3.4 2023.0.3.0 2023.0.3 3.3.4 2023.0.1.0 2023.0.1 3.2.4 2023.0.0.0-RC1 2023.0.0 3.2.0 2022.0.0.0 2022.0.0 3.0.2 2022.0.0.0-RC2 2022.0.0-RC2 3.0.2 2022.0.0.0-RC1 2022.0.0-RC1 3.0.0 2021.0.6.2 2021.0.9 2.7.18 2021.0.6.0 2021.0.9 2.7.18 2021.0.5.0 2021.0.5 2.6.13 2021.0.4.0 2021.0.4 2.6.11 2021.0.1.0 2021.0.1 2.6.3 2021.1 2020.0.1 2.4.2 📌 旧版本（RELEASE 命名） Spring Cloud Alibaba Spring Cloud Spring Boot 2.2.10.RELEASE Hoxton.SR12 2.3.12.RELEASE 2.2.9.RELEASE Hoxton.SR12 2.3.12.RELEASE 2.2.8.RELEASE Hoxton.SR12 2.3.12.RELEASE 2.2.7.RELEASE Hoxton.SR12 2.3.12.RELEASE 2.2.6.RELEASE Hoxton.SR9 2.3.2.RELEASE 2.2.1.RELEASE Hoxton.SR3 2.2.5.RELEASE 2.2.0.RELEASE Hoxton.RELEASE 2.2.X.RELEASE 2.1.4.RELEASE Greenwich.SR6 2.1.13.RELEASE 2.1.2.RELEASE Greenwich 2.1.X.RELEASE 2.0.4.RELEASE ⛔ Finchley 2.0.X.RELEASE 1.5.1.RELEASE ⛔ Edgware 1.5.X.RELEASE ⛔ 表示已停止维护\n✅ 常用推荐组合 场景 Spring Boot SCA 版本 最新稳定（JDK 17/21） 3.3.4 2023.0.3.2 主流稳定（JDK 17） 3.2.4 2023.0.1.0 稳定过渡（JDK 11/17） 2.7.18 2021.0.6.2 老项目维护（JDK 8） 2.6.13 2021.0.5.0 经典老项目（JDK 8） 2.3.12.RELEASE 2.2.10.RELEASE ⚠️ 重要说明 版本命名规则（2021+ 之后）：SCA 版本号 = ${Spring Cloud 版本}.${SCA 自身小版本} 务必三者匹配，不要随意混搭 如有疑问请以 官方 Releases 页 为准 ","permalink":"https://lv-blog.pages.dev/posts/programming/backend/sca-springcloud-springboot-version-mapping/","summary":"\u003cblockquote\u003e\n\u003cp\u003e数据来源：\u003ca href=\"https://github.com/alibaba/spring-cloud-alibaba/wiki/%E7%89%88%E6%9C%AC%E8%AF%B4%E6%98%8E\"\u003eSpring Cloud Alibaba 官方 Wiki\u003c/a\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch1 id=\"springboot-334-之后\"\u003eSpringBoot 3.3.4 之后\u003c/h1\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eSpring Cloud Alibaba Version\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eSpring Cloud Version\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eSpring Boot Version\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2025.1.0.0\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2025.1.0\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e4.0.0\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eSpring Cloud Alibaba Version\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eSpring Cloud Version\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eSpring Boot Version\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2025.0.0.0\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2025.0.0\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3.5.0\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"组件版本关系\"\u003e组件版本关系\u003c/h2\u003e\n\u003cp\u003e每个 Spring Cloud Alibaba 版本及其自身所适配的各组件对应版本如下表所示：\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eSpring Cloud Alibaba Version\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eSentinel Version\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eNacos Version\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eRocketMQ Version\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eSchedulerX Version\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eSeata Version\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2025.1.0.0\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1.8.9\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3.1.1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5.3.1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1.13.3\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2.5.0\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2025.0.0.0\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1.8.9\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3.0.3\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e5.3.1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1.13.1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2.5.0\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"依赖引入\"\u003e依赖引入\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;dependencyManagement\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;dependencies\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#75715e\"\u003e\u0026lt;!-- Spring Boot 版本统一管理 --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003eorg.springframework.boot\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003espring-boot-dependencies\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e3.5.0\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;type\u0026gt;\u003c/span\u003epom\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/type\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;scope\u0026gt;\u003c/span\u003eimport\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/scope\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#75715e\"\u003e\u0026lt;!-- Spring Cloud 官方版本管理 --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003eorg.springframework.cloud\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003espring-cloud-dependencies\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e2025.0.0\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;type\u0026gt;\u003c/span\u003epom\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/type\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;scope\u0026gt;\u003c/span\u003eimport\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/scope\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#75715e\"\u003e\u0026lt;!-- Spring Cloud Alibaba 阿里生态统一版本 --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003ecom.alibaba.cloud\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003espring-cloud-alibaba-dependencies\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e2025.0.0.0\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;type\u0026gt;\u003c/span\u003epom\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/type\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;scope\u0026gt;\u003c/span\u003eimport\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/scope\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/dependencies\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/dependencyManagement\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch1 id=\"springboot-334-及以前\"\u003eSpringBoot 3.3.4 及以前\u003c/h1\u003e\n\u003chr\u003e\n\u003ch2 id=\"-新版本calendar-versioning推荐使用\"\u003e📌 新版本（Calendar Versioning，推荐使用）\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eSpring Cloud Alibaba\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eSpring Cloud\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eSpring Boot\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e2023.0.3.2\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2023.0.3\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e3.3.4\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e2023.0.3.0\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2023.0.3\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e3.3.4\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e2023.0.1.0\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2023.0.1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e3.2.4\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2023.0.0.0-RC1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2023.0.0\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3.2.0\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e2022.0.0.0\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2022.0.0\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e3.0.2\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2022.0.0.0-RC2\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2022.0.0-RC2\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3.0.2\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2022.0.0.0-RC1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2022.0.0-RC1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3.0.0\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e2021.0.6.2\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2021.0.9\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e2.7.18\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e2021.0.6.0\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2021.0.9\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e2.7.18\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e2021.0.5.0\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2021.0.5\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e2.6.13\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e2021.0.4.0\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2021.0.4\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e2.6.11\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e2021.0.1.0\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2021.0.1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e2.6.3\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2021.1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2020.0.1\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2.4.2\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch2 id=\"-旧版本release-命名\"\u003e📌 旧版本（RELEASE 命名）\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003eSpring Cloud Alibaba\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eSpring Cloud\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eSpring Boot\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e2.2.10.RELEASE\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHoxton.SR12\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e2.3.12.RELEASE\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2.2.9.RELEASE\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHoxton.SR12\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2.3.12.RELEASE\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2.2.8.RELEASE\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHoxton.SR12\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2.3.12.RELEASE\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2.2.7.RELEASE\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHoxton.SR12\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2.3.12.RELEASE\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2.2.6.RELEASE\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHoxton.SR9\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2.3.2.RELEASE\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2.2.1.RELEASE\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHoxton.SR3\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2.2.5.RELEASE\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2.2.0.RELEASE\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eHoxton.RELEASE\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2.2.X.RELEASE\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e2.1.4.RELEASE\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eGreenwich.SR6\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e2.1.13.RELEASE\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2.1.2.RELEASE\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eGreenwich\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2.1.X.RELEASE\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e2.0.4.RELEASE ⛔\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eFinchley\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e2.0.X.RELEASE\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e1.5.1.RELEASE ⛔\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eEdgware\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e1.5.X.RELEASE\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cblockquote\u003e\n\u003cp\u003e⛔ 表示已停止维护\u003c/p\u003e","title":"SCA SpringCloud SpringBoot版本对应表"},{"content":" 技术栈：Spring Boot 3 · Spring Cloud Alibaba · OpenFeign · JWT · Gateway · Nacos\n如何使用这份文档 面试讲项目，最忌讳从代码细节讲起。正确顺序是：\n先讲它解决什么问题 → 再讲整体架构 → 然后按请求流程串一遍 → 最后应对深挖。\n本文档按这个顺序组织。带 🎤 标记的引用框，是可以基本照着说的话术。\n一、一句话概述（开场用） 🎤 我做了一套基于 Spring Cloud Alibaba 的微服务认证系统。它把「认证」这件事拆成三个角色：auth 服务负责签发令牌、网关负责统一校验令牌、各业务服务通过请求头拿到用户身份。整体用 JWT 做无状态认证，服务之间用 OpenFeign 通信，靠 Nacos 做服务发现。\n关键词锚点（面试官会顺着这些追问，要准备好）：无状态 JWT、网关统一鉴权、OpenFeign 远程调用、Nacos 服务发现、BCrypt 密码加密、职责分离。\n二、它解决什么问题（讲动机） 背景：系统是微服务架构，有多个独立服务（auth、user-service、archive-service 等）。如果每个服务各自做一遍登录校验，会有两个问题：\n重复：每个服务都写一遍鉴权逻辑，改规则要改很多处。 有状态难扩展：传统 session 存在单台服务器内存里，多实例之间不共享，扩容困难。 解决思路：「网关统一安检 + JWT 无状态令牌」。所有请求先经过网关校验令牌，业务服务不再重复鉴权；令牌本身自包含用户信息，验证只需用密钥本地验签，不依赖服务端存储。\n🎤 类比机场：网关是安检口，严格查一次护照（验签）；过了安检给你贴个写着身份的手环（请求头里的用户 id）；后面登机口、免税店（各业务服务）只看手环，不再查护照。验签只做一次，业务服务零负担。\n三、整体架构（讲骨架） 模块划分：项目是 Maven 多模块微服务，核心模块及职责如下。\n模块 类型 职责 gateway 网关 所有请求入口；统一校验 JWT；按路由转发到后端服务 auth 认证服务 登录、注册；验密码；签发 JWT。自己不连数据库 user-service 业务服务 管理用户数据（tb_user 表的增删改查） archive-service 业务服务 档案业务；从请求头读取当前用户身份 api 契约模块 存放各服务的 Feign 接口声明 + 配套 DTO（被各服务依赖） common 基建底座 统一返回 Result、全局配置、ThreadLocal 用户上下文等 一个关键设计——auth 与 user-service 分离：auth 只管「认证动作」（验密码、发令牌），不碰数据库；它要用户数据时，通过 OpenFeign 远程调用 user-service 去查。user-service 只管「用户数据」，不掺和登录逻辑。\n🎤 为什么不合成一个？因为职责不同：auth 管「身份核验这个动作」，user-service 管「用户数据怎么存取」。拆开后，auth 可以专注鉴权，user-service 可以专注数据，互不污染。这也是为什么 auth 里没有任何数据库依赖。\n四、三条核心流程（面试主线，按这个讲） 讲项目最有效的方式是「按请求流程走一遍」。三条流程：注册、登录、带令牌访问业务。\n4.1 注册流程 用户提交 手机号 + 密码到 auth 服务。 查重：auth 通过 OpenFeign 调 user-service，确认手机号没被注册。 加密：auth 用 passwordEncoder.encode() 把明文密码做 BCrypt 哈希，得到密文。 存储：auth 把密文通过 OpenFeign 传给 user-service，由它存进 tb_user 表。 设计要点：密码加密在 auth 做，user-service 拿到的已是密文，只负责存、不负责加密——加密是认证逻辑，存储是数据逻辑。\n🎤 密码绝不存明文。BCrypt 是单向哈希，像把肉打成肉馅——能加密、不能还原。即使数据库泄露，攻击者也拿不到原始密码。而且 BCrypt 自带随机盐，同一个密码每次加密结果都不同，能有效防彩虹表。\n4.2 登录流程 用户提交 手机号 + 密码。 查用户：auth 通过 OpenFeign 按手机号调 user-service，拿到含密文密码的用户信息。 验密码：用 passwordEncoder.matches(明文, 密文) 比对。 签发 JWT：验证通过后，用 jjwt 把用户 id 装进令牌载荷，用密钥签名，返回 token。 验密码的核心认知：不是「解密数据库密文再比对」（哈希不可逆），而是把用户这次输入的明文再哈希一次，比对两个密文是否一致。这套逻辑被 matches() 封装好了。\n🎤 JWT 分三段：头部（算法）、载荷（用户 id、过期时间）、签名（防伪）。载荷是 Base64 编码、明文可见的，所以只放用户 id 这类「被看到也无害」的信息，绝不放密码。防伪靠第三段签名——用只有服务端知道的密钥生成，别人改了载荷也算不出对的签名。\n4.3 带令牌访问业务流程 前端带令牌：登录后保存 token，之后每次请求放在 Authorization 请求头里。 网关校验：请求先到 gateway，GlobalFilter 拦截，做三件事：判断白名单 → 取令牌 → 验签 + 查过期。 塞入身份：验证通过后，网关从令牌解析出用户 id，写进自定义请求头 X-User-Id。 路由转发：按路由规则（lb://服务名）经 Nacos 找到目标服务，转发过去。 业务取身份：业务服务用拦截器从 X-User-Id 取出用户 id，存进 ThreadLocal，业务代码任意处用 UserContext.getUserId() 获取。 🎤 白名单很关键：登录、注册接口本身不能验令牌——用户还没登录哪来的令牌，否则就死锁了。所以这两个接口在网关直接放行。\n五、关键技术点（应对深挖，这些最容易被追问） 5.1 为什么 JWT 验签放在网关，而不是每个服务？ 性能：验签只在网关做一次，业务服务零鉴权负担。 解耦：业务服务不需要懂 JWT、不需要密钥，代码极简，只读请求头。 JWT 自包含的优势：验签是用密钥本地计算，不查库、不远程调用，所以快；这也是 JWT 相比 session 在分布式下的核心优势——无状态、可水平扩展。 5.2 网关为什么用 WebFlux（响应式）？ 网关用的是 Spring Cloud Gateway，底层是 WebFlux 非阻塞模型，和普通 Servlet 服务（auth/user-service）不同。\nServlet（阻塞）：一个请求占一个线程，等 IO 时线程干等、浪费，高并发下线程易耗尽。 WebFlux（非阻塞）：线程不干等，等 IO 时去服务别的请求，少量线程扛大量并发。 为什么适合网关：网关流量最大、又主要在「等后端转发」，正好契合非阻塞；代价是网关里不能写阻塞代码，而 JWT 验签是纯本地计算、天然不阻塞。 5.3 用户身份为什么用请求头传，不能在网关用 ThreadLocal？ 核心原因：网关和业务服务是两个独立进程，各有各的内存。ThreadLocal 只在进程内有效，网关存了业务服务跨不过去、读不到。\n跨进程必须靠网络可传输的载体：请求头跟着 HTTP 请求在网络上传输，能从网关「飞」到业务服务。 两段 ThreadLocal 是分开的：网关解析 id → 写请求头（跨进程的桥）→ 业务服务读请求头 → 存进自己进程的 ThreadLocal 供本地取用。 🎤 这是分布式的一个根本原则：跨服务传数据只能用「网络可传输的载体」（请求头、请求体、消息队列），不能用「进程内内存」（ThreadLocal、静态变量、单例）——进程内的东西困在自己进程里。\n5.4 ThreadLocal 为什么必须 remove？ 线程复用会串数据：Web 服务器线程是线程池复用的。线程处理完用户 A 的请求不会销毁，会被回收去处理用户 B。如果不清 ThreadLocal，B 可能读到 A 残留的用户 id，造成「张冠李戴」的严重 bug。\n做法：在拦截器的 afterCompletion（请求彻底结束、哪怕异常也会执行）里调 removeUser() 清理。\n5.5 一个安全隐患（主动提，是加分项） 业务服务信任 X-User-Id 请求头。如果有人绕过网关直接访问业务服务、自己伪造这个头，就能冒充他人。\n解决：保证业务服务只能通过网关访问、不直接对外暴露端口（网络隔离 + 内部鉴权），所有请求必经网关安检，X-User-Id 才可信。\n六、技术栈一览 领域 技术选型 作用 微服务框架 Spring Boot 3 + Spring Cloud Alibaba 2023 基础框架 服务发现 Nacos 服务注册与发现，服务间靠服务名互找 服务调用 OpenFeign + LoadBalancer 声明式远程调用 + 负载均衡 网关 Spring Cloud Gateway (WebFlux) 统一入口、鉴权、路由 认证 JWT (jjwt) 无状态令牌的签发与校验 密码 BCrypt (Spring Security Crypto) 密码单向哈希 持久层 MyBatis-Plus + PostgreSQL 用户数据存储 配置管理 Nacos Config 集中配置（密钥等可放共享配置） 七、完整数据流（一张图讲清） 登录拿令牌：\n前端 → 网关(白名单放行) → auth → Feign查user-service → 验密码 → 签发JWT → 返回token 带令牌访问：\n前端(带token) → 网关 GlobalFilter(验签 + 查过期) → 解析 userId 塞入 X-User-Id → 路由(lb://) 经 Nacos 找服务 → 业务服务拦截器读 header 存 ThreadLocal → 业务代码 UserContext.getUserId() 八、面试自检清单（能答上说明你真懂了） 为什么密码不能明文存？BCrypt 哪两个特性让它安全？ JWT 三段分别是什么？载荷为什么不能放密码？防伪靠什么？ 登录验密码是「解密比对」还是「再哈希比对」？为什么？ 为什么 auth 不直接连数据库？它怎么拿用户数据？ 为什么验签放网关一次就够？JWT 相比 session 的优势？ 网关为什么用 WebFlux？它和 Servlet 模型的区别？ 用户身份为什么用请求头传，不能在网关存 ThreadLocal？ ThreadLocal 为什么必须 remove？不清会怎样？ X-User-Id 有什么安全隐患？怎么防？ api 模块和 common 模块的定位区别？Feign 接口为什么放 api？ 提示：讲项目时，先讲「解决什么问题」和「整体架构」，再按三条流程串，深挖问题随机应变。切忌一上来就贴代码。\n","permalink":"https://lv-blog.pages.dev/posts/programming/backend/microservice-auth-architecture-and-interview/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e技术栈\u003c/strong\u003e：Spring Boot 3 · Spring Cloud Alibaba · OpenFeign · JWT · Gateway · Nacos\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"如何使用这份文档\"\u003e如何使用这份文档\u003c/h2\u003e\n\u003cp\u003e面试讲项目，最忌讳从代码细节讲起。正确顺序是：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e先讲它解决什么问题 → 再讲整体架构 → 然后按请求流程串一遍 → 最后应对深挖。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e本文档按这个顺序组织。带 🎤 标记的引用框，是可以基本照着说的话术。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"一一句话概述开场用\"\u003e一、一句话概述（开场用）\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e🎤 我做了一套基于 Spring Cloud Alibaba 的微服务认证系统。它把「认证」这件事拆成三个角色：\u003cstrong\u003eauth 服务负责签发令牌、网关负责统一校验令牌、各业务服务通过请求头拿到用户身份\u003c/strong\u003e。整体用 JWT 做无状态认证，服务之间用 OpenFeign 通信，靠 Nacos 做服务发现。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cstrong\u003e关键词锚点\u003c/strong\u003e（面试官会顺着这些追问，要准备好）：无状态 JWT、网关统一鉴权、OpenFeign 远程调用、Nacos 服务发现、BCrypt 密码加密、职责分离。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"二它解决什么问题讲动机\"\u003e二、它解决什么问题（讲动机）\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e背景\u003c/strong\u003e：系统是微服务架构，有多个独立服务（auth、user-service、archive-service 等）。如果每个服务各自做一遍登录校验，会有两个问题：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e重复\u003c/strong\u003e：每个服务都写一遍鉴权逻辑，改规则要改很多处。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e有状态难扩展\u003c/strong\u003e：传统 session 存在单台服务器内存里，多实例之间不共享，扩容困难。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e解决思路\u003c/strong\u003e：「网关统一安检 + JWT 无状态令牌」。所有请求先经过网关校验令牌，业务服务不再重复鉴权；令牌本身自包含用户信息，验证只需用密钥本地验签，不依赖服务端存储。\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e🎤 类比机场：网关是安检口，严格查一次护照（验签）；过了安检给你贴个写着身份的手环（请求头里的用户 id）；后面登机口、免税店（各业务服务）只看手环，不再查护照。验签只做一次，业务服务零负担。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"三整体架构讲骨架\"\u003e三、整体架构（讲骨架）\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e模块划分\u003c/strong\u003e：项目是 Maven 多模块微服务，核心模块及职责如下。\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e模块\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e类型\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e职责\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003egateway\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e网关\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e所有请求入口；统一校验 JWT；按路由转发到后端服务\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eauth\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e认证服务\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e登录、注册；验密码；签发 JWT。\u003cstrong\u003e自己不连数据库\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003euser-service\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e业务服务\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e管理用户数据（tb_user 表的增删改查）\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003earchive-service\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e业务服务\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e档案业务；从请求头读取当前用户身份\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eapi\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e契约模块\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e存放各服务的 Feign 接口声明 + 配套 DTO（被各服务依赖）\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003ecommon\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e基建底座\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e统一返回 Result、全局配置、ThreadLocal 用户上下文等\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cstrong\u003e一个关键设计——auth 与 user-service 分离\u003c/strong\u003e：auth 只管「认证动作」（验密码、发令牌），不碰数据库；它要用户数据时，通过 OpenFeign 远程调用 user-service 去查。user-service 只管「用户数据」，不掺和登录逻辑。\u003c/p\u003e","title":"微服务认证系统的架构梳理和面试应对"},{"content":"每个写过后端的人，迟早都要跟\u0026quot;登录\u0026quot;打一场硬仗。它看起来是个小功能——一个用户名、一个密码、一个按钮——但凡是真正做过的人都知道，这是整个系统里坑最深、改起来最痛、出事最致命的一块。\n它麻烦，不是因为技术有多难，而是因为它卡在三方利益的正中间：用户想省事，产品想拉新，安全想严防死守。这三件事天然打架。你每往其中一边挪一寸，另外两边就开始喊疼。\n这篇文章想把这件\u0026quot;麻烦事\u0026quot;摊开讲清楚：主流厂商现在都怎么做、每种做法烂在哪、未来可能怎么变，以及如果你今天就要动手，应该怎么落地。\n一、先看战场：主流厂商现在都在用什么 如果你今天注册任何一个稍微正经点的 App，会发现登录方式早就不是\u0026quot;账号 + 密码\u0026quot;一条路了。现在的主流玩法大致可以分成五类。\n1. 手机号 + 短信验证码 这是中国互联网的绝对统治者。点开微博、抖音、美团、拼多多，第一屏几乎清一色是\u0026quot;输入手机号 → 收验证码 → 进\u0026quot;。\n它能赢，是因为它一次性解决了三个问题：手机号天然实名（运营商帮你做了 KYC）、不用记密码、注册和登录是同一个动作。对产品经理来说，这意味着注册转化率极高——少一个\u0026quot;设置密码\u0026quot;的步骤，就少漏一批用户。\n代价是：你把身份的命脉，交给了运营商和短信通道。\n2. 邮箱 + 密码 这是全球（尤其欧美）的默认范式。GitHub、Google、Notion、几乎所有 SaaS 都以邮箱为账号主体。\n邮箱的好处是全球通用、跨国可用、不依赖运营商、可以承载找回流程。一个邮箱地址几乎就是你在互联网上的\u0026quot;主键\u0026quot;。坏处后面细说。\n3. 第三方登录（OAuth / 社交登录） \u0026ldquo;用微信登录\u0026quot;\u0026ldquo;Sign in with Google\u0026quot;\u0026ldquo;Continue with Apple\u0026rdquo;——本质上是把身份认证外包给一个你已经信任的大平台。\n它的杀手锏是：用户一次都不用输。点一下，授权，进去了。对开发者来说，你还省掉了自己存密码的风险（密码根本不经过你的服务器）。代价是你被绑在了平台生态上，而且用户的账号体系实际上不在你手里。\n4. 魔法链接（Magic Link） 无密码的一种：你输入邮箱，系统给你发一封带一次性链接的邮件，点开即登录。Slack、Medium 早期都靠这个。\n它把\u0026quot;记密码\u0026quot;这件事彻底删掉了，安全模型简单清晰。但它把整个登录体验的流畅度，押在了\u0026quot;邮件能不能秒到\u0026quot;上——而邮件这玩意，慢起来能慢到你怀疑人生。\n5. Passkey / WebAuthn（通行密钥） 这是最新、也是各大厂商正在猛推的方向。Apple、Google、Microsoft 已经全面支持。它基于公私钥密码学，用你设备上的生物识别（指纹、Face ID）来完成认证，服务器端根本不存任何可被盗的密码。\n这是目前公认\u0026quot;理论上最优\u0026quot;的方案，我们最后单独讲。\n下面这张图，把这五种方式按\u0026quot;用户省心程度\u0026quot;和\u0026quot;安全强度\u0026quot;两个维度摆一摆，你能直观看到它们各自的生态位：\ngraph TD A[\"登录方式光谱\"] --\u003e B[\"短信验证码省心高 / 安全中\"] A --\u003e C[\"邮箱密码省心低 / 安全中\"] A --\u003e D[\"第三方登录省心高 / 安全中高\"] A --\u003e E[\"魔法链接省心中 / 安全中\"] A --\u003e F[\"Passkey省心高 / 安全高\"] B --\u003e B1[\"依赖运营商换号即失联\"] C --\u003e C1[\"密码要记容易被撞库\"] D --\u003e D1[\"绑死大平台账号不在自己手里\"] E --\u003e E1[\"押注邮件时效慢到怀疑人生\"] F --\u003e F1[\"体验最好但迁移设备是痛点\"] style A fill:#1a1a2e,color:#fff style F fill:#16213e,color:#7fff7f 二、麻烦的本质：每一种方式都在某处偷偷塌方 上面每种方式听起来都还行，但它们都有一个藏在水面下的塌方点。用户平时感觉不到，一旦踩中，体验直接归零。\n麻烦一：手机号——你换号的那一天，账号就成了孤儿 短信验证码最大的命门，是它把你的数字身份焊死在了一串运营商号码上。\n想想这些真实场景：你出国了，国内号停机了；你换运营商了，号没保住；你换号了，忘了把几十个 App 一个个改绑。等你哪天想登回某个老账号，发现收验证码的那个号早就不是你的了——而很多产品压根没给\u0026quot;换绑手机号\u0026quot;留一条不依赖旧手机的退路。\n更糟的是安全层面：手机号会被回收再分配。运营商把你停用的号过段时间发给别人，新机主一条验证码就能登进你曾经的账号。还有 SIM Swap 攻击——攻击者通过社工骗运营商把你的号码补办到他的 SIM 卡上，你所有靠短信做二次验证的账号瞬间裸奔。\n短信验证码看着方便，本质上是把账号安全外包给了你管不到的运营商体系。\n麻烦二：邮箱——慢、容易丢、而且没人天天看 邮箱的麻烦比手机隐蔽，但同样致命。\n第一是时效性差到离谱。注册要等验证邮件、找回密码要等链接，理论上几秒，实际可能几分钟，甚至直接躺进垃圾箱。你做过的产品里，多少用户卡在\u0026quot;没收到邮件\u0026quot;这一步直接流失了？\n第二是邮箱本身会丢。公司邮箱离职就没了，学校邮箱毕业就废了，老邮箱密码忘了进不去了。而一旦你的账号主键是这个邮箱，邮箱一丢，账号跟着陪葬。\n第三是习惯问题。在移动端时代，相当一部分用户根本不看邮件了，尤其是国内用户。让他\u0026quot;去邮箱点确认链接\u0026rdquo;，对他来说是一次彻底的体验中断——他得切到另一个 App、翻找邮件、点链接、再切回来。这个流程每多一步，就掉一批人。\n麻烦三：密码——人脑和安全要求是天生的敌人 这是所有麻烦里最古老、也最无解的一个：安全的密码人记不住，能记住的密码不安全。\n安全规则要求你：每个网站用不同的密码、要够长、要混大小写数字符号、还要定期换。可人的大脑做不到这件事。于是现实世界里发生的是：\n所有网站用同一个密码——于是一个网站被拖库，你全网沦陷（这就是\u0026quot;撞库攻击\u0026quot;的温床）； 用 Password123! 这种符合规则但毫无强度的密码； 把密码写在便签、记事本、备忘录里——安全防线直接物理崩溃； 真忘了，就走\u0026quot;找回密码\u0026rdquo;——而找回密码这条路，本身又绕回到了手机或邮箱的麻烦上。 密码的根本困境在于：它要求人脑去做一件人脑不擅长的事——存储和管理大量高熵随机串。 这件事从设计上就注定要失败。\n把这三条麻烦串起来看，你会发现一个残酷的真相——它们其实在互相踢皮球：\ngraph TD A[\"密码忘了\"] --\u003e B[\"走找回流程\"] B --\u003e C[\"验证手机号\"] C --\u003e D[\"手机号换了/停了\"] D --\u003e E[\"改走邮箱验证\"] E --\u003e F[\"邮箱也进不去了\"] F --\u003e G[\"联系人工客服\"] G --\u003e H[\"证明你是你极度痛苦\"] style A fill:#2d1b1b,color:#ff9999 style H fill:#2d1b1b,color:#ff5555 每一种登录方式的\u0026quot;找回\u0026quot;机制，都依赖另一种方式做兜底。可一旦你同时丢了手机和邮箱，整条链就断了，最后只能走人工客服——去证明\u0026quot;你就是你\u0026quot;，而这恰恰是身份系统里最难、体验最差的一环。\n三、可能的变局：扫脸登录，是答案吗？ 既然密码、手机、邮箱都这么麻烦，很多人第一反应是：那直接刷脸不就完了？\n听起来很美，但这里要把一个关键概念拆清楚——刷脸有两种，它们的安全含义天差地别。\n第一种：本地生物识别。 比如 iPhone 的 Face ID。你的脸从来不会离开你的手机，它只在设备的安全芯片里比对，结果只是\u0026quot;是/否\u0026quot;。这种是安全的，而且它正是 Passkey 体验的基础——你以为在\u0026quot;刷脸登录\u0026quot;，其实刷脸只是用来解锁你设备里那把私钥而已。\n第二种：服务器端人脸识别。 你的脸被拍下来、传到服务器、跟数据库里的人脸比对。这种非常危险，原因很简单：\n密码泄露了，你可以改密码。你的脸泄露了，你改不了。\n生物特征是你身上不可撤销的东西。一旦某个公司的人脸库被拖了，受害者一辈子都没法\u0026quot;重置\u0026quot;自己的脸。再加上活体检测可以被照片、视频、深度伪造（Deepfake）攻破，把\u0026quot;脸\u0026quot;直接当成服务器端的登录凭据，是在制造一个无法挽回的灾难。\n所以\u0026quot;扫脸直接登录\u0026quot;这个说法，对一半错一半：\n✅ 对的部分：用脸在本地解锁设备上的密钥——这就是未来。 ❌ 错的部分：把脸当成上传服务器的密码——这是灾难。 除了刷脸，真正在重塑登录格局的变局还有这几个方向：\nPasskey 的全面普及。 这是目前最被看好的\u0026quot;终局方案\u0026quot;。FIDO 联盟、Apple、Google、Microsoft 在合力推。它的核心思路是：把登录从\u0026quot;你知道什么\u0026quot;（密码）彻底切换到\u0026quot;你拥有什么\u0026quot;（你的设备 + 设备里的私钥）。\nPasswordless 成为默认。 越来越多产品在新用户注册时根本不再提供\u0026quot;设密码\u0026quot;选项，直接走验证码或 Passkey。密码正在从\u0026quot;必选项\u0026quot;退化成\u0026quot;兼容旧用户的遗留项\u0026quot;。\n去中心化身份（DID）。 更激进的设想：你的身份不再属于任何一家公司，而是装在你自己掌控的钱包里，登录任何服务都用它授权。理念很先进，但离大规模落地还很远，目前更多停留在概念和小范围实验。\n下面这张图，是我对登录技术演进路线的判断：\ngraph TD A[\"密码时代你知道什么\"] --\u003e B[\"短信/邮箱验证码削弱密码依赖\"] B --\u003e C[\"第三方登录外包身份\"] C --\u003e D[\"Passkey 时代你拥有什么\"] D --\u003e E[\"去中心化身份你掌控什么\"] A -.弱点.-\u003e A1[\"会被撞库/钓鱼\"] B -.弱点.-\u003e B1[\"依赖运营商/邮件\"] C -.弱点.-\u003e C1[\"绑死平台\"] D -.优势.-\u003e D1[\"防钓鱼/无密码\"] E -.愿景.-\u003e E1[\"身份自主\"] style A fill:#2d1b1b,color:#ff9999 style D fill:#16213e,color:#7fff7f style E fill:#1a1a2e,color:#9999ff 四、可以落地的最佳实践 讲了这么多，落到实处——如果你今天就要给一个新产品设计登录，到底该怎么做？这里给一套务实、可落地、分场景的建议，不是理论上的完美，而是工程上的最优。\n原则一：账号主体用稳定标识，而不是手机号 这是最重要的一条架构决策。不要把手机号或邮箱直接当成用户主键。\n正确的做法是：内部用一个永不变的 user_id（比如雪花 ID 或 UUID）作为用户唯一标识，手机号、邮箱、第三方 OpenID 等等，全部作为**可绑定、可解绑、可更换的\u0026quot;登录凭据\u0026quot;**挂在这个 user_id 下面。\n这样，用户换手机号？改一条绑定记录就行，账号本身毫发无损。这一个设计，就能避免后面 90% 的\u0026quot;换号找不回账号\u0026quot;的客诉。\ngraph TD A[\"user_id永久不变的主键\"] --\u003e B[\"手机号凭据可换绑\"] A --\u003e C[\"邮箱凭据可换绑\"] A --\u003e D[\"微信 OpenID可解绑\"] A --\u003e E[\"Passkey 凭据可多设备\"] A --\u003e F[\"密码凭据可选/兼容\"] style A fill:#16213e,color:#7fff7f 原则二：分场景选主登录方式 没有一种登录方式适合所有产品，按你的实际场景选：\n面向国内大众消费者：主推手机验证码，因为用户习惯、转化率最高。但务必把\u0026quot;换绑手机号不依赖旧手机\u0026quot;的退路做好。 面向全球 / SaaS / 开发者：主推邮箱 + Passkey，辅以 Google/GitHub 第三方登录。 对安全要求高的（金融、企业内部）：直接上 Passkey 为主 + 强制二次验证。 小程序 / 微信生态内：直接用微信授权登录，别让用户再输手机号。 原则三：能上 Passkey 就上，但要留好退路 Passkey 是方向，新产品应该把它作为首选提供。但它今天还有一个现实痛点：设备迁移和账号恢复。私钥在设备上，换手机、丢手机时怎么办？\n所以落地策略是：Passkey 做主力，但永远给用户至少一条独立的恢复通道（比如绑定的邮箱、或一组一次性恢复码）。不要做成\u0026quot;丢了设备就彻底进不去\u0026quot;的死局。\n原则四：永远不要自己存明文密码 如果你的产品确实保留了密码登录（兼容老用户），那记死一条铁律：\n绝不存明文，绝不用 MD5/SHA1 这种快速哈希； 用 bcrypt / scrypt / Argon2 这类专门为密码设计的、带盐、可调计算成本的慢哈希算法； 密码强度校验看熵，别再用\u0026quot;必须含大小写数字符号\u0026quot;这种反人类的规则去折磨用户——一个足够长的密码短语，比 P@ss1! 安全得多。 原则五：安全与体验的平衡——风险驱动 最后一条是心法。安全和体验是跷跷板，但你不必每次都两边都拉满。聪明的做法是风险自适应（Risk-based Authentication）：\n用户在常用设备、常用地点、常规时间登录 → 少打扰，验证码都可以免； 检测到异常（新设备、异地、深夜、高风险操作）→ 加码验证，要求二次确认。 把摩擦力用在刀刃上，而不是均匀地撒给每个用户。这样既不会因为太松而出事，也不会因为太严而把正常用户逼走。\n写在最后 登录这件事的麻烦，归根结底是因为它要同时回答一个最朴素、也最难的问题：\n\u0026ldquo;你怎么证明你是你？\u0026rdquo;\n人类社会用了几千年，从印章、签名、身份证一路演进到今天，都没把这个问题彻底解决。指望软件用一个登录框搞定，本来就是奢望。\n但方向是清晰的：我们正在从\u0026quot;考验记忆力\u0026quot;的密码时代，走向\u0026quot;考验拥有权\u0026quot;的密钥时代。 未来的登录，理想状态应该是——你不需要记任何东西，不需要等任何短信邮件，只需要证明\u0026quot;这台设备是我的、这个生物特征是我的\u0026quot;，剩下的交给密码学。\n作为开发者，我们能做的，就是在这条演进路上，别给用户挖那些本可以避免的坑：别把账号焊死在手机号上，别强迫用户记反人类的密码，别把别人的脸存进自己的数据库，也别做成丢了设备就万劫不复的死局。\n把登录这件麻烦事做\u0026quot;不麻烦\u0026quot;，本身就是一种很高级的工程素养。\n","permalink":"https://lv-blog.pages.dev/posts/programming/backend/the-hassle-of-login/","summary":"\u003cp\u003e每个写过后端的人，迟早都要跟\u0026quot;登录\u0026quot;打一场硬仗。它看起来是个小功能——一个用户名、一个密码、一个按钮——但凡是真正做过的人都知道，这是整个系统里\u003cstrong\u003e坑最深、改起来最痛、出事最致命\u003c/strong\u003e的一块。\u003c/p\u003e\n\u003cp\u003e它麻烦，不是因为技术有多难，而是因为它卡在三方利益的正中间：用户想省事，产品想拉新，安全想严防死守。这三件事天然打架。你每往其中一边挪一寸，另外两边就开始喊疼。\u003c/p\u003e\n\u003cp\u003e这篇文章想把这件\u0026quot;麻烦事\u0026quot;摊开讲清楚：主流厂商现在都怎么做、每种做法烂在哪、未来可能怎么变，以及如果你今天就要动手，应该怎么落地。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"一先看战场主流厂商现在都在用什么\"\u003e一、先看战场：主流厂商现在都在用什么\u003c/h2\u003e\n\u003cp\u003e如果你今天注册任何一个稍微正经点的 App，会发现登录方式早就不是\u0026quot;账号 + 密码\u0026quot;一条路了。现在的主流玩法大致可以分成五类。\u003c/p\u003e\n\u003ch3 id=\"1-手机号--短信验证码\"\u003e1. 手机号 + 短信验证码\u003c/h3\u003e\n\u003cp\u003e这是中国互联网的\u003cstrong\u003e绝对统治者\u003c/strong\u003e。点开微博、抖音、美团、拼多多，第一屏几乎清一色是\u0026quot;输入手机号 → 收验证码 → 进\u0026quot;。\u003c/p\u003e\n\u003cp\u003e它能赢，是因为它一次性解决了三个问题：手机号天然实名（运营商帮你做了 KYC）、不用记密码、注册和登录是同一个动作。对产品经理来说，这意味着\u003cstrong\u003e注册转化率极高\u003c/strong\u003e——少一个\u0026quot;设置密码\u0026quot;的步骤，就少漏一批用户。\u003c/p\u003e\n\u003cp\u003e代价是：你把身份的命脉，交给了运营商和短信通道。\u003c/p\u003e\n\u003ch3 id=\"2-邮箱--密码\"\u003e2. 邮箱 + 密码\u003c/h3\u003e\n\u003cp\u003e这是全球（尤其欧美）的默认范式。GitHub、Google、Notion、几乎所有 SaaS 都以邮箱为账号主体。\u003c/p\u003e\n\u003cp\u003e邮箱的好处是\u003cstrong\u003e全球通用、跨国可用、不依赖运营商、可以承载找回流程\u003c/strong\u003e。一个邮箱地址几乎就是你在互联网上的\u0026quot;主键\u0026quot;。坏处后面细说。\u003c/p\u003e\n\u003ch3 id=\"3-第三方登录oauth--社交登录\"\u003e3. 第三方登录（OAuth / 社交登录）\u003c/h3\u003e\n\u003cp\u003e\u0026ldquo;用微信登录\u0026quot;\u0026ldquo;Sign in with Google\u0026quot;\u0026ldquo;Continue with Apple\u0026rdquo;——本质上是把身份认证\u003cstrong\u003e外包\u003c/strong\u003e给一个你已经信任的大平台。\u003c/p\u003e\n\u003cp\u003e它的杀手锏是：用户一次都不用输。点一下，授权，进去了。对开发者来说，你还省掉了自己存密码的风险（密码根本不经过你的服务器）。代价是你被绑在了平台生态上，而且用户的账号体系实际上\u003cstrong\u003e不在你手里\u003c/strong\u003e。\u003c/p\u003e\n\u003ch3 id=\"4-魔法链接magic-link\"\u003e4. 魔法链接（Magic Link）\u003c/h3\u003e\n\u003cp\u003e无密码的一种：你输入邮箱，系统给你发一封带一次性链接的邮件，点开即登录。Slack、Medium 早期都靠这个。\u003c/p\u003e\n\u003cp\u003e它把\u0026quot;记密码\u0026quot;这件事彻底删掉了，安全模型简单清晰。但它把整个登录体验的流畅度，押在了\u0026quot;邮件能不能秒到\u0026quot;上——而邮件这玩意，慢起来能慢到你怀疑人生。\u003c/p\u003e\n\u003ch3 id=\"5-passkey--webauthn通行密钥\"\u003e5. Passkey / WebAuthn（通行密钥）\u003c/h3\u003e\n\u003cp\u003e这是最新、也是各大厂商正在猛推的方向。Apple、Google、Microsoft 已经全面支持。它基于公私钥密码学，用你设备上的生物识别（指纹、Face ID）来完成认证，\u003cstrong\u003e服务器端根本不存任何可被盗的密码\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e这是目前公认\u0026quot;理论上最优\u0026quot;的方案，我们最后单独讲。\u003c/p\u003e\n\u003cp\u003e下面这张图，把这五种方式按\u0026quot;用户省心程度\u0026quot;和\u0026quot;安全强度\u0026quot;两个维度摆一摆，你能直观看到它们各自的生态位：\u003c/p\u003e\n\u003cpre class=\"mermaid\"\u003egraph TD\n    A[\"登录方式光谱\"] --\u003e B[\"短信验证码\u003cbr/\u003e省心高 / 安全中\"]\n    A --\u003e C[\"邮箱密码\u003cbr/\u003e省心低 / 安全中\"]\n    A --\u003e D[\"第三方登录\u003cbr/\u003e省心高 / 安全中高\"]\n    A --\u003e E[\"魔法链接\u003cbr/\u003e省心中 / 安全中\"]\n    A --\u003e F[\"Passkey\u003cbr/\u003e省心高 / 安全高\"]\n\n    B --\u003e B1[\"依赖运营商\u003cbr/\u003e换号即失联\"]\n    C --\u003e C1[\"密码要记\u003cbr/\u003e容易被撞库\"]\n    D --\u003e D1[\"绑死大平台\u003cbr/\u003e账号不在自己手里\"]\n    E --\u003e E1[\"押注邮件时效\u003cbr/\u003e慢到怀疑人生\"]\n    F --\u003e F1[\"体验最好\u003cbr/\u003e但迁移设备是痛点\"]\n\n    style A fill:#1a1a2e,color:#fff\n    style F fill:#16213e,color:#7fff7f\n\u003c/pre\u003e\n\u003chr\u003e\n\u003ch2 id=\"二麻烦的本质每一种方式都在某处偷偷塌方\"\u003e二、麻烦的本质：每一种方式都在某处偷偷塌方\u003c/h2\u003e\n\u003cp\u003e上面每种方式听起来都还行，但它们都有一个\u003cstrong\u003e藏在水面下的塌方点\u003c/strong\u003e。用户平时感觉不到，一旦踩中，体验直接归零。\u003c/p\u003e","title":"说一说登录这件麻烦事"},{"content":"通用 依赖管理 父 pom.xml 固定三版本管控 版本参考\n\u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- 1. SpringBoot 基础版本 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.0\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- 2. Spring Cloud 云原生基础 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2025.0.0\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- 3. Spring Cloud Alibaba 阿里全套依赖核心（关键！） --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-alibaba-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2025.0.0.0\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; 启动类 @MapperScan(\u0026#34;asia.liminality.user.mapper\u0026#34;) @SpringBootApplication @Slf4j public class UserApplication { public static void main(String[] args) throws UnknownHostException { ConfigurableApplicationContext app = SpringApplication.run(UserApplication.class, args); Environment env = app.getEnvironment(); String protocol = \u0026#34;http\u0026#34;; if (env.getProperty(\u0026#34;server.ssl.key-store\u0026#34;) != null) { protocol = \u0026#34;https\u0026#34;; } log.info(\u0026#34;--/\\n---------------------------------------------------------------------------------------\\n\\t\u0026#34; + \u0026#34;Application \u0026#39;{}\u0026#39; is running! Access URLs:\\n\\t\u0026#34; + \u0026#34;Local: \\t\\t{}://localhost:{}\\n\\t\u0026#34; + \u0026#34;External: \\t{}://{}:{}\\n\\t\u0026#34; + \u0026#34;Profile(s): \\t{}\u0026#34; + \u0026#34;\\n---------------------------------------------------------------------------------------\u0026#34;, env.getProperty(\u0026#34;spring.application.name\u0026#34;), protocol, env.getProperty(\u0026#34;server.port\u0026#34;), protocol, InetAddress.getLocalHost().getHostAddress(), env.getProperty(\u0026#34;server.port\u0026#34;), env.getActiveProfiles()); } } domain DTO Result import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class Result\u0026lt;T\u0026gt; { private int code; private String message; private T data; public static \u0026lt;T\u0026gt; Result\u0026lt;T\u0026gt; success(T data) { return new Result\u0026lt;\u0026gt;(200, \u0026#34;success\u0026#34;, data); } public static \u0026lt;T\u0026gt; Result\u0026lt;T\u0026gt; success() { return new Result\u0026lt;\u0026gt;(200, \u0026#34;success\u0026#34;, null); } public static \u0026lt;T\u0026gt; Result\u0026lt;T\u0026gt; failure(String message) { return new Result\u0026lt;\u0026gt;(500, message, null); } public static \u0026lt;T\u0026gt; Result\u0026lt;T\u0026gt; failure(int code, String message) { return new Result\u0026lt;\u0026gt;(code, message, null); } public static \u0026lt;T\u0026gt; Result\u0026lt;T\u0026gt; failure(String message, T data) { return new Result\u0026lt;\u0026gt;(500, message, data); } } Spring Cloud Gateway AuthGlobalFilter @Slf4j @RequiredArgsConstructor @Component public class AuthGlobalFilter implements GlobalFilter, Ordered { private final JwtUtil jwtUtil; private static final List\u0026lt;String\u0026gt; WHITE_LIST = List.of( \u0026#34;/auth/user/login\u0026#34;, \u0026#34;/auth/user/register\u0026#34; ); // 判断是否在白名单内 private boolean isWhiteList(String path){ return WHITE_LIST.stream().anyMatch(path::startsWith); } private Mono\u0026lt;Void\u0026gt; reject(ServerWebExchange exchange){ exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } @Override public Mono\u0026lt;Void\u0026gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String path = request.getPath().toString(); if (isWhiteList(path)) { return chain.filter(exchange); } String token = request.getHeaders().getFirst(\u0026#34;Authorization\u0026#34;); // 令牌为空，拒绝访问 if( token == null || token.isEmpty() ){ return reject( exchange ); } // 验证令牌 try { Integer userId = jwtUtil.parseToken(token); // TODO userId 塞入请求头 log.info(\u0026#34;🚪 塞入请求头\u0026#34;); } catch (Exception e) { return reject(exchange); } return chain.filter( exchange ); } // 过滤器优先级，越小越靠前 @Override public int getOrder() { return -1; } } ","permalink":"https://lv-blog.pages.dev/posts/programming/backend/code-snippets/spring-boot-common-snippets/","summary":"\u003ch1 id=\"通用\"\u003e通用\u003c/h1\u003e\n\u003ch2 id=\"依赖管理\"\u003e依赖管理\u003c/h2\u003e\n\u003ch3 id=\"父-pomxml-固定三版本管控\"\u003e父 pom.xml 固定三版本管控\u003c/h3\u003e\n\u003cp\u003e\u003ca href=\"/posts/programming/backend/sca-springcloud-springboot-version-mapping/\"\u003e版本参考\u003c/a\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;dependencyManagement\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;dependencies\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#75715e\"\u003e\u0026lt;!-- 1. SpringBoot 基础版本 --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003eorg.springframework.boot\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003espring-boot-dependencies\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e3.5.0\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;type\u0026gt;\u003c/span\u003epom\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/type\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;scope\u0026gt;\u003c/span\u003eimport\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/scope\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#75715e\"\u003e\u0026lt;!-- 2. Spring Cloud 云原生基础 --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003eorg.springframework.cloud\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003espring-cloud-dependencies\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e2025.0.0\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;type\u0026gt;\u003c/span\u003epom\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/type\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;scope\u0026gt;\u003c/span\u003eimport\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/scope\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#75715e\"\u003e\u0026lt;!-- 3. Spring Cloud Alibaba 阿里全套依赖核心（关键！） --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003ecom.alibaba.cloud\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003espring-cloud-alibaba-dependencies\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e2025.0.0.0\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;type\u0026gt;\u003c/span\u003epom\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/type\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003e\u0026lt;scope\u0026gt;\u003c/span\u003eimport\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/scope\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;/dependencies\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/dependencyManagement\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"启动类\"\u003e启动类\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e@MapperScan\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;asia.liminality.user.mapper\u0026#34;\u003c/span\u003e)\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e@SpringBootApplication\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e@Slf4j\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eUserApplication\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003evoid\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003emain\u003c/span\u003e(String\u003cspan style=\"color:#f92672\"\u003e[]\u003c/span\u003e args) \u003cspan style=\"color:#66d9ef\"\u003ethrows\u003c/span\u003e UnknownHostException {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        ConfigurableApplicationContext app \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e SpringApplication.\u003cspan style=\"color:#a6e22e\"\u003erun\u003c/span\u003e(UserApplication.\u003cspan style=\"color:#a6e22e\"\u003eclass\u003c/span\u003e, args);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        Environment env \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e app.\u003cspan style=\"color:#a6e22e\"\u003egetEnvironment\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        String protocol \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;http\u0026#34;\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (env.\u003cspan style=\"color:#a6e22e\"\u003egetProperty\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;server.ssl.key-store\u0026#34;\u003c/span\u003e) \u003cspan style=\"color:#f92672\"\u003e!=\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enull\u003c/span\u003e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            protocol \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;https\u0026#34;\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        log.\u003cspan style=\"color:#a6e22e\"\u003einfo\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;--/\\n---------------------------------------------------------------------------------------\\n\\t\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                        \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Application \u0026#39;{}\u0026#39; is running! Access URLs:\\n\\t\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                        \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Local: \\t\\t{}://localhost:{}\\n\\t\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                        \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;External: \\t{}://{}:{}\\n\\t\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                        \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Profile(s): \\t{}\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                        \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\\n---------------------------------------------------------------------------------------\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                env.\u003cspan style=\"color:#a6e22e\"\u003egetProperty\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;spring.application.name\u0026#34;\u003c/span\u003e),\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                protocol,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                env.\u003cspan style=\"color:#a6e22e\"\u003egetProperty\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;server.port\u0026#34;\u003c/span\u003e),\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                protocol,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                InetAddress.\u003cspan style=\"color:#a6e22e\"\u003egetLocalHost\u003c/span\u003e().\u003cspan style=\"color:#a6e22e\"\u003egetHostAddress\u003c/span\u003e(),\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                env.\u003cspan style=\"color:#a6e22e\"\u003egetProperty\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;server.port\u0026#34;\u003c/span\u003e),\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e                env.\u003cspan style=\"color:#a6e22e\"\u003egetActiveProfiles\u003c/span\u003e());\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"domain\"\u003edomain\u003c/h2\u003e\n\u003ch3 id=\"dto\"\u003eDTO\u003c/h3\u003e\n\u003ch4 id=\"result\"\u003eResult\u003c/h4\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003eimport\u003c/span\u003e lombok.AllArgsConstructor;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003eimport\u003c/span\u003e lombok.Data;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003eimport\u003c/span\u003e lombok.NoArgsConstructor;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e@Data\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e@NoArgsConstructor\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e@AllArgsConstructor\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eResult\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003eT\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eprivate\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e code;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eprivate\u003c/span\u003e String message;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eprivate\u003c/span\u003e T data;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003eT\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e Result\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003eT\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003esuccess\u003c/span\u003e(T data) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e Result\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u0026gt;\u003c/span\u003e(200, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;success\u0026#34;\u003c/span\u003e, data);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003eT\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e Result\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003eT\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003esuccess\u003c/span\u003e() {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e Result\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u0026gt;\u003c/span\u003e(200, \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;success\u0026#34;\u003c/span\u003e, \u003cspan style=\"color:#66d9ef\"\u003enull\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003eT\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e Result\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003eT\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003efailure\u003c/span\u003e(String message) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e Result\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u0026gt;\u003c/span\u003e(500, message, \u003cspan style=\"color:#66d9ef\"\u003enull\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003eT\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e Result\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003eT\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003efailure\u003c/span\u003e(\u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e code, String message) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e Result\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u0026gt;\u003c/span\u003e(code, message, \u003cspan style=\"color:#66d9ef\"\u003enull\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003eT\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e Result\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003eT\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003efailure\u003c/span\u003e(String message, T data) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e Result\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u0026gt;\u003c/span\u003e(500, message, data);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch1 id=\"spring-cloud-gateway\"\u003eSpring Cloud Gateway\u003c/h1\u003e\n\u003ch2 id=\"authglobalfilter\"\u003eAuthGlobalFilter\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e@Slf4j\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e@RequiredArgsConstructor\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e@Component\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eAuthGlobalFilter\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eimplements\u003c/span\u003e GlobalFilter, Ordered {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eprivate\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003efinal\u003c/span\u003e JwtUtil jwtUtil;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eprivate\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003estatic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003efinal\u003c/span\u003e List\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003eString\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e WHITE_LIST \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e List.\u003cspan style=\"color:#a6e22e\"\u003eof\u003c/span\u003e(\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/auth/user/login\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;/auth/user/register\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    );\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 判断是否在白名单内\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eprivate\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eboolean\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eisWhiteList\u003c/span\u003e(String path){\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e WHITE_LIST.\u003cspan style=\"color:#a6e22e\"\u003estream\u003c/span\u003e().\u003cspan style=\"color:#a6e22e\"\u003eanyMatch\u003c/span\u003e(path::startsWith);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eprivate\u003c/span\u003e Mono\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003eVoid\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003ereject\u003c/span\u003e(ServerWebExchange exchange){\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        exchange.\u003cspan style=\"color:#a6e22e\"\u003egetResponse\u003c/span\u003e().\u003cspan style=\"color:#a6e22e\"\u003esetStatusCode\u003c/span\u003e(HttpStatus.\u003cspan style=\"color:#a6e22e\"\u003eUNAUTHORIZED\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e exchange.\u003cspan style=\"color:#a6e22e\"\u003egetResponse\u003c/span\u003e().\u003cspan style=\"color:#a6e22e\"\u003esetComplete\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003e@Override\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e Mono\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003eVoid\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003efilter\u003c/span\u003e(ServerWebExchange exchange, GatewayFilterChain chain) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        ServerHttpRequest request \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e exchange.\u003cspan style=\"color:#a6e22e\"\u003egetRequest\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        String path \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e request.\u003cspan style=\"color:#a6e22e\"\u003egetPath\u003c/span\u003e().\u003cspan style=\"color:#a6e22e\"\u003etoString\u003c/span\u003e();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e (isWhiteList(path)) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e chain.\u003cspan style=\"color:#a6e22e\"\u003efilter\u003c/span\u003e(exchange);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        String token \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e request.\u003cspan style=\"color:#a6e22e\"\u003egetHeaders\u003c/span\u003e().\u003cspan style=\"color:#a6e22e\"\u003egetFirst\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;Authorization\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#75715e\"\u003e// 令牌为空，拒绝访问\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e( token \u003cspan style=\"color:#f92672\"\u003e==\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enull\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e||\u003c/span\u003e token.\u003cspan style=\"color:#a6e22e\"\u003eisEmpty\u003c/span\u003e() ){\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e reject( exchange );\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#75715e\"\u003e// 验证令牌\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003etry\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            Integer userId \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e jwtUtil.\u003cspan style=\"color:#a6e22e\"\u003eparseToken\u003c/span\u003e(token);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#75715e\"\u003e// TODO userId 塞入请求头\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            log.\u003cspan style=\"color:#a6e22e\"\u003einfo\u003c/span\u003e(\u003cspan style=\"color:#e6db74\"\u003e\u0026#34;🚪 塞入请求头\u0026#34;\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        } \u003cspan style=\"color:#66d9ef\"\u003ecatch\u003c/span\u003e (Exception e) {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e reject(exchange);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e chain.\u003cspan style=\"color:#a6e22e\"\u003efilter\u003c/span\u003e( exchange );\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 过滤器优先级，越小越靠前\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#a6e22e\"\u003e@Override\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eint\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003egetOrder\u003c/span\u003e() {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e1;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"Spring Boot 常用代码片段"},{"content":"9月22日 星期一 DAY 1 突然患病 早上起床右耳耳朵闷，左耳正常。以为就是耳朵堵，中耳炎之类，会自然恢复，没太留意。\n9月23日 星期二 DAY 2 这一天发现耳朵除了闷以外，听外界说话还有回声，感觉事情不太对。但是依然没有往严重方向上想，上班要紧。\n9月25日 星期四 DAY4 看了很多视频，感觉可能是突发性耳聋。决定第二天去医院。此时已经错过了网上流传的所谓“72小时”黄金时间。\n9月27日 星期六 DAY6 医院诊断 上午去南京中西医结合医院诊断。医生说考虑是一个突发性耳聋。一直很纠结医生说的“考虑说”是啥意思？他自己也不确定吗？\n医生说要吃药，目前只是低频下降明显，不算特别严重。\n后来开了一些药，要我遵循医嘱去吃，并且要放松自己，按时睡觉，健康生活。\n这一天我进行了充足的睡眠。\n9月28日 星期日 DAY6 早上醒来后，发现耳朵闷好像有所缓解，但是右耳明显耳鸣，一直有那种地铁经过的声音，但是音量不算太大。\n9月30日 星期二 DAY9 早上醒来后，突然发现两个耳朵都听力下降了。缓了一会儿发现左耳好了，右耳还是老样子。把我吓得不轻。\n感觉听力没有明显的好转。但是闷闷的感觉似乎有改善。\n右耳有点感觉，好像是药物起作用了，不确定。\n10月8日 星期三 从这一天开始耳鸣变得明显。耳鸣的声音感觉在400-500Hz左右会一直持续。\n病情没有一丝好转。\n10月9日 星期四 我到南京江苏人民医院再次进行耳部检查。本次检查后，进行了输液。\n10月10日 第二次挂水。今日症状仍无任何缓解。\n10月11日 第三次挂水。症状未减轻。中低音耳鸣明显，伴随地铁列车嗖嗖经过的声音。\n10月12日 第四次挂水。\n10月13日 当晚因工作忙碌未挂水。\n10月14日 到医院复查，发现右耳听力几乎恢复正常（右耳进入25db及格线内）。开了药继续吃。\n11月21日 突聋发作已29天，距初次治疗已24天 中低音耳鸣明显减弱，几乎听不到。现在听到的是音量很低的收音机调不到台的噪音。125Hz、250Hz的声音听到的音量有较小差异。右耳对125Hz、250Hz（或者就直接说是250Hz以下）的声音的听力没有左耳好，且伴随明显的音调变化。500Hz及以上左右耳几乎一致，但依然伴随微弱的音调变化。\n10月23日 明显好转，右耳几乎听不到任何噪音，也没有耳鸣现象。目前和左耳的唯一区别就是125hz-500hz区间听力有微弱下降，伴随音调变化。\n11月12日 距离突发性耳聋发作已经51天，距离突发性耳聋初次治疗已经46天。目前右耳和左耳比125hz有5db的差距，250hz有10db的差距，其他频段几乎没有差别。在打电话时，用右耳明显有复听现象。一般生活，和之前相比，没有明显感觉到复听。\n","permalink":"https://lv-blog.pages.dev/posts/life/sudden-sensorineural-hearing-loss-treatment-process/","summary":"\u003ch4 id=\"9月22日-星期一-day-1-突然患病\"\u003e9月22日 星期一 DAY 1 突然患病\u003c/h4\u003e\n\u003cp\u003e早上起床右耳耳朵闷，左耳正常。以为就是耳朵堵，中耳炎之类，会自然恢复，没太留意。\u003c/p\u003e\n\u003ch4 id=\"9月23日-星期二-day-2\"\u003e9月23日 星期二 DAY 2\u003c/h4\u003e\n\u003cp\u003e这一天发现耳朵除了闷以外，听外界说话还有回声，感觉事情不太对。但是依然没有往严重方向上想，上班要紧。\u003c/p\u003e\n\u003ch4 id=\"9月25日-星期四-day4\"\u003e9月25日 星期四 DAY4\u003c/h4\u003e\n\u003cp\u003e看了很多视频，感觉可能是突发性耳聋。决定第二天去医院。此时已经错过了网上流传的所谓“72小时”黄金时间。\u003c/p\u003e\n\u003ch4 id=\"9月27日-星期六-day6-医院诊断\"\u003e9月27日 星期六 DAY6 医院诊断\u003c/h4\u003e\n\u003cp\u003e上午去南京中西医结合医院诊断。医生说考虑是一个突发性耳聋。一直很纠结医生说的“考虑说”是啥意思？他自己也不确定吗？\u003c/p\u003e\n\u003cp\u003e医生说要吃药，目前只是低频下降明显，不算特别严重。\u003c/p\u003e\n\u003cp\u003e后来开了一些药，要我遵循医嘱去吃，并且要放松自己，按时睡觉，健康生活。\u003c/p\u003e\n\u003cp\u003e这一天我进行了充足的睡眠。\u003c/p\u003e\n\u003ch4 id=\"9月28日-星期日-day6\"\u003e9月28日 星期日 DAY6\u003c/h4\u003e\n\u003cp\u003e早上醒来后，发现耳朵闷好像有所缓解，但是右耳明显耳鸣，一直有那种地铁经过的声音，但是音量不算太大。\u003c/p\u003e\n\u003ch4 id=\"9月30日-星期二--day9\"\u003e9月30日 星期二  DAY9\u003c/h4\u003e\n\u003cp\u003e早上醒来后，突然发现两个耳朵都听力下降了。缓了一会儿发现左耳好了，右耳还是老样子。把我吓得不轻。\u003c/p\u003e\n\u003cp\u003e感觉听力没有明显的好转。但是闷闷的感觉似乎有改善。\u003c/p\u003e\n\u003cp\u003e右耳有点感觉，好像是药物起作用了，不确定。\u003c/p\u003e\n\u003ch4 id=\"10月8日-星期三\"\u003e10月8日 星期三\u003c/h4\u003e\n\u003cp\u003e从这一天开始耳鸣变得明显。耳鸣的声音感觉在400-500Hz左右会一直持续。\u003c/p\u003e\n\u003cp\u003e病情没有一丝好转。\u003c/p\u003e\n\u003ch4 id=\"10月9日-星期四\"\u003e10月9日 星期四\u003c/h4\u003e\n\u003cp\u003e我到南京江苏人民医院再次进行耳部检查。本次检查后，进行了输液。\u003c/p\u003e\n\u003ch4 id=\"10月10日\"\u003e10月10日\u003c/h4\u003e\n\u003cp\u003e第二次挂水。今日症状仍无任何缓解。\u003c/p\u003e\n\u003ch4 id=\"10月11日\"\u003e10月11日\u003c/h4\u003e\n\u003cp\u003e第三次挂水。症状未减轻。中低音耳鸣明显，伴随地铁列车嗖嗖经过的声音。\u003c/p\u003e\n\u003ch4 id=\"10月12日\"\u003e10月12日\u003c/h4\u003e\n\u003cp\u003e第四次挂水。\u003c/p\u003e\n\u003ch4 id=\"10月13日\"\u003e10月13日\u003c/h4\u003e\n\u003cp\u003e当晚因工作忙碌未挂水。\u003c/p\u003e\n\u003ch4 id=\"10月14日\"\u003e10月14日\u003c/h4\u003e\n\u003cp\u003e到医院复查，发现右耳听力几乎恢复正常（右耳进入25db及格线内）。开了药继续吃。\u003c/p\u003e\n\u003ch4 id=\"11月21日-突聋发作已29天距初次治疗已24天\"\u003e11月21日 突聋发作已29天，距初次治疗已24天\u003c/h4\u003e\n\u003cp\u003e中低音耳鸣明显减弱，几乎听不到。现在听到的是音量很低的收音机调不到台的噪音。125Hz、250Hz的声音听到的音量有较小差异。右耳对125Hz、250Hz（或者就直接说是250Hz以下）的声音的听力没有左耳好，且伴随明显的音调变化。500Hz及以上左右耳几乎一致，但依然伴随微弱的音调变化。\u003c/p\u003e\n\u003ch4 id=\"10月23日\"\u003e10月23日\u003c/h4\u003e\n\u003cp\u003e明显好转，右耳几乎听不到任何噪音，也没有耳鸣现象。目前和左耳的唯一区别就是125hz-500hz区间听力有微弱下降，伴随音调变化。\u003c/p\u003e\n\u003ch4 id=\"11月12日\"\u003e11月12日\u003c/h4\u003e\n\u003cp\u003e距离突发性耳聋发作已经51天，距离突发性耳聋初次治疗已经46天。目前右耳和左耳比125hz有5db的差距，250hz有10db的差距，其他频段几乎没有差别。在打电话时，用右耳明显有复听现象。一般生活，和之前相比，没有明显感觉到复听。\u003c/p\u003e","title":"突发性耳聋治疗过程"},{"content":"Java 的 LocalDateTime 和 PostgreSQL 的时间类型，\u0026ldquo;说的不是同一种时间\u0026rdquo;。\n先搞清楚 PostgreSQL 的两种时间类型 TIMESTAMP → 不带时区，就是个裸时间 \u0026#34;2026-06-25 10:00:00\u0026#34; TIMESTAMPTZ → 带时区，内部存 UTC，查询时按会话时区转换 Java 这边 LocalDateTime → 没有时区概念，就是个裸时间 ZonedDateTime → 带时区 OffsetDateTime → 带偏移量（如 +08:00） Instant → UTC 时间戳 为什么会报错 PostgreSQL JDBC 驱动（特别是新版本 42.x+）对类型匹配非常严格：\nflowchart TD A[Java LocalDateTime] --\u0026gt; B[JDBC驱动] B --\u0026gt; C{PostgreSQL列类型} C --\u0026gt;|TIMESTAMP| D[✅ 可以匹配] C --\u0026gt;|TIMESTAMPTZ| E[❌ 类型不匹配报错] 你的列如果是 TIMESTAMPTZ（带时区），但 Java 传的是 LocalDateTime（无时区），驱动不知道该用哪个时区换算，就直接拒绝了。\n常见的三种报错 Cannot convert LocalDateTime to TIMESTAMPTZ Bad value for type timestamp/date column is of type timestamp with time zone but expression is of type timestamp 解决方案 方案一：改 Java 类型（推荐）\n// 把实体类里的字段改成 private OffsetDateTime createdAt; // 或 private Instant createdAt; OffsetDateTime 和 PostgreSQL 的 TIMESTAMPTZ 天然匹配。\n方案二：改 PostgreSQL 列类型\n-- 如果你不需要时区，把列改成不带时区 ALTER TABLE tb_archive ALTER COLUMN created_at TYPE TIMESTAMP; 然后 LocalDateTime 就能正常用了。\n方案三：强制类型转换（临时方案，不推荐）\n// MyBatis 层手动转 createdAt.atOffset(ZoneOffset.UTC) 最简建议 flowchart TD A[你的场景] --\u0026gt; B{需要时区吗} B --\u0026gt;|需要，比如多国用户| C[PostgreSQL用TIMESTAMPTZ\\nJava用OffsetDateTime] B --\u0026gt;|不需要，单时区项目| D[PostgreSQL用TIMESTAMP\\nJava用LocalDateTime] ","permalink":"https://lv-blog.pages.dev/posts/programming/backend/bugs-solutions/postgresql-java-localdatetime-incompatibility/","summary":"\u003cp\u003e\u003cstrong\u003eJava 的 \u003ccode\u003eLocalDateTime\u003c/code\u003e 和 PostgreSQL 的时间类型，\u0026ldquo;说的不是同一种时间\u0026rdquo;。\u003c/strong\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"先搞清楚-postgresql-的两种时间类型\"\u003e先搞清楚 PostgreSQL 的两种时间类型\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eTIMESTAMP           → 不带时区，就是个裸时间 \u0026#34;2026-06-25 10:00:00\u0026#34;\nTIMESTAMPTZ         → 带时区，内部存 UTC，查询时按会话时区转换\n\u003c/code\u003e\u003c/pre\u003e\u003chr\u003e\n\u003ch2 id=\"java-这边\"\u003eJava 这边\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eLocalDateTime       → 没有时区概念，就是个裸时间\nZonedDateTime       → 带时区\nOffsetDateTime      → 带偏移量（如 +08:00）\nInstant             → UTC 时间戳\n\u003c/code\u003e\u003c/pre\u003e\u003chr\u003e\n\u003ch2 id=\"为什么会报错\"\u003e为什么会报错\u003c/h2\u003e\n\u003cp\u003ePostgreSQL JDBC 驱动（特别是新版本 42.x+）对类型匹配\u003cstrong\u003e非常严格\u003c/strong\u003e：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eflowchart TD\n    A[Java LocalDateTime] --\u0026gt; B[JDBC驱动]\n    B --\u0026gt; C{PostgreSQL列类型}\n    C --\u0026gt;|TIMESTAMP| D[✅ 可以匹配]\n    C --\u0026gt;|TIMESTAMPTZ| E[❌ 类型不匹配报错]\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e你的列如果是 \u003ccode\u003eTIMESTAMPTZ\u003c/code\u003e（带时区），但 Java 传的是 \u003ccode\u003eLocalDateTime\u003c/code\u003e（无时区），驱动不知道该用哪个时区换算，就直接拒绝了。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"常见的三种报错\"\u003e常见的三种报错\u003c/h2\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eCannot convert LocalDateTime to TIMESTAMPTZ\nBad value for type timestamp/date\ncolumn is of type timestamp with time zone but expression is of type timestamp\n\u003c/code\u003e\u003c/pre\u003e\u003chr\u003e\n\u003ch2 id=\"解决方案\"\u003e解决方案\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e方案一：改 Java 类型（推荐）\u003c/strong\u003e\u003c/p\u003e","title":"PostgreSQL和java的LocalDateTime不兼容的问题"},{"content":"在 Spring Boot 生态中，Swagger 2.0（通常使用 Foxfire 依赖）和 Swagger 3.0（通常使用 Springdoc-openapi 依赖，基于 OpenAPI 3 规范）的注解发生了很大变化。\n以下是 Swagger 2.0 与 Swagger 3.0（OpenAPI 3）的常用注释完整对应表：\n1. 核心注解对应表 功能描述 Swagger 2.0 注解 (io.swagger.annotations) Swagger 3.0 注解 (io.swagger.v3.oas.annotations) 备注说明 标记控制器类 @Api(tags = \u0026quot;用户接口\u0026quot;) @Tag(name = \u0026quot;用户接口\u0026quot;) 3.0 中移除了 description 属性，统一使用 name 标记接口方法 @ApiOperation(value = \u0026quot;获取用户\u0026quot;) @Operation(summary = \u0026quot;获取用户\u0026quot;) 3.0 中 value 变更为 summary 入参实体类 @ApiModel(value = \u0026quot;用户对象\u0026quot;) @Schema(description = \u0026quot;用户对象\u0026quot;) 3.0 极大简化，统一使用 @Schema 实体类属性 @ApiModelProperty(value = \u0026quot;姓名\u0026quot;) @Schema(description = \u0026quot;姓名\u0026quot;) 同上，合并为了 @Schema 忽略某个属性 @ApiModelProperty(hidden = true) @Schema(hidden = true) 忽略整个类/方法 @ApiIgnore @Hidden 用于不想暴露在文档中的接口或参数 2. 请求参数注解对应表 对于方法入参（如 URL 路径参数、Query 参数等），3.0 引入了更具结构化的配置：\n功能描述 Swagger 2.0 注解 Swagger 3.0 注解 单个容器参数 @ApiImplicitParam @Parameter 多个容器参数 @ApiImplicitParams({ ... }) @Parameters({ ... }) 普通方法参数 @ApiParam(value = \u0026quot;用户ID\u0026quot;) @Parameter(description = \u0026quot;用户ID\u0026quot;) 💡 注意（@Parameter 的使用变化）： 在 3.0 中，如果是获取路径参数（@PathVariable）或查询参数（@RequestParam），可以直接在参数前加 @Parameter：\n// Swagger 3.0 示例 @GetMapping(\u0026#34;/{id}\u0026#34;) public User getUser(@Parameter(description = \u0026#34;用户ID\u0026#34;, example = \u0026#34;1\u0026#34;) @PathVariable Long id) 3. 响应状态码注解对应表 功能描述 Swagger 2.0 注解 Swagger 3.0 注解 单个通用响应 @ApiResponse(code = 404, message = \u0026quot;未找到\u0026quot;) @ApiResponse(responseCode = \u0026quot;404\u0026quot;, description = \u0026quot;未找到\u0026quot;) 多个通用响应 @ApiResponses({ ... }) @ApiResponses({ ... }) (名称未变，内部组件变了) 4. 依赖迁移对比 (补充) 除了注解组件包路径从 io.swagger.annotations.* 变更为 io.swagger.v3.oas.annotations.* 之外，如果你在做项目升级，Maven 依赖也需要同步修改：\nSwagger 2.x (Springfox): \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.springfox\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;springfox-swagger2\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.x.x\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; Swagger 3.x (Springdoc): \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springdoc\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;springdoc-openapi-starter-webmvc-ui\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.x.x (对应 Spring Boot 3)\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; ","permalink":"https://lv-blog.pages.dev/posts/programming/backend/bugs-solutions/swagger-2-3-annotation-comparison/","summary":"\u003cp\u003e在 Spring Boot 生态中，Swagger 2.0（通常使用 \u003ccode\u003eFoxfire\u003c/code\u003e 依赖）和 Swagger 3.0（通常使用 \u003ccode\u003eSpringdoc-openapi\u003c/code\u003e 依赖，基于 OpenAPI 3 规范）的注解发生了很大变化。\u003c/p\u003e\n\u003cp\u003e以下是 Swagger 2.0 与 Swagger 3.0（OpenAPI 3）的常用注释完整对应表：\u003c/p\u003e\n\u003ch3 id=\"1-核心注解对应表\"\u003e1. 核心注解对应表\u003c/h3\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e功能描述\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eSwagger 2.0 注解 (io.swagger.annotations)\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eSwagger 3.0 注解 (io.swagger.v3.oas.annotations)\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e备注说明\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e标记控制器类\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e@Api(tags = \u0026quot;用户接口\u0026quot;)\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e@Tag(name = \u0026quot;用户接口\u0026quot;)\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3.0 中移除了 \u003ccode\u003edescription\u003c/code\u003e 属性，统一使用 \u003ccode\u003ename\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e标记接口方法\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e@ApiOperation(value = \u0026quot;获取用户\u0026quot;)\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e@Operation(summary = \u0026quot;获取用户\u0026quot;)\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3.0 中 \u003ccode\u003evalue\u003c/code\u003e 变更为 \u003ccode\u003esummary\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e入参实体类\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e@ApiModel(value = \u0026quot;用户对象\u0026quot;)\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e@Schema(description = \u0026quot;用户对象\u0026quot;)\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e3.0 极大简化，统一使用 \u003ccode\u003e@Schema\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e实体类属性\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e@ApiModelProperty(value = \u0026quot;姓名\u0026quot;)\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e@Schema(description = \u0026quot;姓名\u0026quot;)\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e同上，合并为了 \u003ccode\u003e@Schema\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e忽略某个属性\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e@ApiModelProperty(hidden = true)\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e@Schema(hidden = true)\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e忽略整个类/方法\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e@ApiIgnore\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003e@Hidden\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e用于不想暴露在文档中的接口或参数\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003ch3 id=\"2-请求参数注解对应表\"\u003e2. 请求参数注解对应表\u003c/h3\u003e\n\u003cp\u003e对于方法入参（如 URL 路径参数、Query 参数等），3.0 引入了更具结构化的配置：\u003c/p\u003e","title":"Swagger2和Swagger3注解对比表"},{"content":" 有的命令一个月可能就用那么几次，不写手册里谁能记得住啊\nVSCode 解决 code-runner Java Output 乱码 \u0026#34;code-runner.executorMap\u0026#34;: { \u0026#34;javascript\u0026#34;: \u0026#34;node\u0026#34;, \u0026#34;java\u0026#34;: \u0026#34;\\\u0026#34;C:/Program Files/Java/jdk-17/bin/java.exe\\\u0026#34; -Dfile.encoding=UTF-8\u0026#34;, PowerShell 快速完成端口转发 netsh interface portproxy add v4tov4 ` listenaddress=0.0.0.0 ` listenport=2375 ` connectaddress=127.0.0.1 ` connectport=2375 查端口占用 杀进程 netstat -aon | findstr :8080 taskkill /PID 39656 /F 自定义常用命令别名 notepad $PROFILE function gacp { param ( [string]$msg = \u0026#34;update\u0026#34; ) git add . git commit -m $msg git push } # 删除当前文件夹下空文件夹 function rme { Get-ChildItem -Directory -Recurse | Where-Object { $_.GetFileSystemInfos().Count -eq 0 } | Remove-Item } Linux Linux开辟虚拟内存（Swap） free -h Linux实现FRP内网穿透 的是让国内用户能连到一台没有公网 IP 的家庭电脑——用的是反向内网穿透:\n国内用户 → 云服务器(有公网IP,域名解析到它)→ frps(服务端,监听公网端口) ↕ frpc主动拨出的加密隧道 家里PopOS笔记本 → frpc(客户端)→ 本地80/443服务 角色分工:frps 装在云服务器(被动等连接的一方,因为它有公网 IP);frpc 装在家里笔记本(主动连出去的一方,因为它在 NAT 后面没有公网 IP)。\nfrps 端配置(云服务器) /root/frp_0.61.0_linux_amd64/frps.toml:\nbindPort = 7000 # 控制端口,frpc就是连这个端口上来注册 auth.method = \u0026#34;token\u0026#34; auth.token = \u0026#34;2b65b0b89f8ff57fcb1a631176605898\u0026#34; # frpc必须带同样的token才能连上 log.to = \u0026#34;/var/log/frps.log\u0026#34; log.level = \u0026#34;info\u0026#34; log.maxDays = 7 systemd 服务 /etc/systemd/system/frps.service:\n[Unit] Description=frp server After=network.target [Service] Type=simple User=root Restart=on-failure RestartSec=5s ExecStart=/root/frp_0.61.0_linux_amd64/frps -c /root/frp_0.61.0_linux_amd64/frps.toml [Install] WantedBy=multi-user.target systemctl enable --now frps 之后开机自启、崩溃自动重启。\nfrpc 端配置(家里笔记本) /etc/frp/frpc.toml:\nserverAddr = \u0026#34;yjsws.cn\u0026#34; # 域名已解析到云服务器,直接用域名 serverPort = 7000 # 对应frps的bindPort auth.method = \u0026#34;token\u0026#34; auth.token = \u0026#34;2b65b0b89f8ff57fcb1a631176605898\u0026#34; # 必须和frps一致 [[proxies]] name = \u0026#34;home-http\u0026#34; type = \u0026#34;tcp\u0026#34; localIP = \u0026#34;127.0.0.1\u0026#34; localPort = 80 # 笔记本本地网站监听的端口 remotePort = 80 # 云服务器上对外暴露的端口 [[proxies]] name = \u0026#34;home-https\u0026#34; type = \u0026#34;tcp\u0026#34; localIP = \u0026#34;127.0.0.1\u0026#34; localPort = 443 remotePort = 443 对应 systemd 服务 /etc/systemd/system/frpc.service:\n[Unit] Description=frp client After=network.target [Service] Type=simple Restart=on-failure RestartSec=5s ExecStart=/usr/local/bin/frpc -c /etc/frp/frpc.toml [Install] WantedBy=multi-user.target 几个关键决策 1. 选了 type = \u0026quot;tcp\u0026quot; 纯透传,没用 type = \u0026quot;http\u0026quot;/\u0026quot;https\u0026quot; 的 vhost 模式 好处是简单直接、字节原样转发,适合单域名测试;代价是 frps 完全不解密流量,TLS 握手是浏览器和笔记本之间直接做的——这也是为什么证书必须装在笔记本上,而不能复用云服务器那张(TLS 到底在哪一端终止,证书就必须在哪一端)。\n如果以后要在同一个 frps 上挂多个域名/多个后端,才需要换成 vhost 模式:\nvhostHTTPPort = 80 vhostHTTPSPort = 443 配合 proxy 里的 customDomains = [\u0026quot;a.com\u0026quot;] 做基于域名的分流,此时 frps 可以在 HTTP 层面按域名转发给不同的后端。\n2. 云服务器的 80/443 端口冲突 之前 nginx 占着这两个端口给 yjsws.cn 提供服务,得先 systemctl stop nginx \u0026amp;\u0026amp; systemctl disable nginx 腾出端口给 frps 直接监听。\n副作用:nginx 自己的 certbot 续期机制也跟着失效了——续期要用 80 端口做 HTTP-01 验证,但 80 现在整个转发给了笔记本,云服务器本地已经没有东西能应答验证请求了。所以证书管理这件事顺势也挪到了笔记本那边(用笔记本上的 nginx + certbot 重新申请,续期时验证请求会通过隧道打到笔记本本地完成)。\n3. 安全性 auth.token 是 frpc 能连上 frps 的唯一门槛,泄露了等于任何人都能借用这条隧道把流量转到别处,得当密码保管。 阿里云的安全组是控制台层面的东西,和本机 iptables 是两码事——本机防火墙放行不代表外部真能连进来,得单独去控制台确认 80/443/7000 入方向规则开了。 7000 控制端口测试期间是开着的,长期使用建议加白名单限制来源 IP,或者收紧到仅允许家里笔记本的出口 IP。 验证方式 tail -f /var/log/frps.log # 看frpc有没有成功login、代理有没有注册成功(\u0026#34;new proxy [home-http] ... success\u0026#34;) ss -tulnp | grep -E \u0026#39;:80 |:443 \u0026#39; # 确认frps确实在监听这两个端口(frpc连上来注册后才会动态绑定,不是一启动就有) curl -o /dev/null -w \u0026#39;%{http_code}\u0026#39; http://yjsws.cn/ # 端到端实测,能拿到笔记本网站的真实响应 其他 # 1Panel curl -sSL https://resource.fit2cloud.com/1panel/package/quick_start.sh -o quick_start.sh \u0026amp;\u0026amp; sudo bash quick_start.sh # zsh sh -c \u0026#34;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)\u0026#34; # 服务器测试 curl -sL yabs.sh | bash # 备份 ssh root@你的VPS_IP \\ \u0026#34;dd if=/dev/vda bs=4M status=progress | gzip\u0026#34; \\ \u0026gt; vps.img.gz Git Windows 一键生成 .gitignore function shell { param( [Parameter(Mandatory=$true)] [string]$Arguments ) $targets = $Arguments -replace \u0026#39; \u0026#39;, \u0026#39;,\u0026#39; $url = \u0026#34;https://www.toptal.com/developers/gitignore/api/$targets\u0026#34; Invoke-WebRequest -Uri $url -OutFile .gitignore Write-Host \u0026#34;[SUCCESS] .gitignore generated for: [$Arguments]\u0026#34; -ForegroundColor Green } 反向代理（Nginx） Nginx 一键部署（Linux） #!/bin/bash # ================================================ # Nginx 一键安装脚本 # 支持: Ubuntu/Debian / CentOS/RHEL/Rocky/Alma # 作者: LOCRIAN_V # ================================================ set -e # -------- 颜色输出 -------- RED=\u0026#39;\\033[0;31m\u0026#39; GREEN=\u0026#39;\\033[0;32m\u0026#39; YELLOW=\u0026#39;\\033[1;33m\u0026#39; CYAN=\u0026#39;\\033[0;36m\u0026#39; NC=\u0026#39;\\033[0m\u0026#39; info() { echo -e \u0026#34;${CYAN}[INFO]${NC} $1\u0026#34;; } success() { echo -e \u0026#34;${GREEN}[OK]${NC} $1\u0026#34;; } warn() { echo -e \u0026#34;${YELLOW}[WARN]${NC} $1\u0026#34;; } error() { echo -e \u0026#34;${RED}[ERR]${NC} $1\u0026#34;; exit 1; } # -------- 检查 root -------- if [[ $EUID -ne 0 ]]; then error \u0026#34;请使用 root 或 sudo 运行此脚本\u0026#34; fi # -------- 检测发行版 -------- detect_os() { if [ -f /etc/os-release ]; then . /etc/os-release OS=$ID OS_VERSION=$VERSION_ID else error \u0026#34;无法识别操作系统，请手动安装 Nginx\u0026#34; fi } # -------- 安装函数 -------- install_nginx_debian() { info \u0026#34;检测到 Debian/Ubuntu 系统 (${OS} ${OS_VERSION})\u0026#34; info \u0026#34;更新软件包列表...\u0026#34; apt-get update -y info \u0026#34;安装依赖...\u0026#34; apt-get install -y curl gnupg2 ca-certificates lsb-release debian-archive-keyring info \u0026#34;添加 Nginx 官方仓库...\u0026#34; curl -fsSL https://nginx.org/keys/nginx_signing.key | gpg --dearmor \\ -o /usr/share/keyrings/nginx-archive-keyring.gpg # 根据发行版选择仓库 if [[ \u0026#34;$OS\u0026#34; == \u0026#34;ubuntu\u0026#34; ]]; then CODENAME=$(lsb_release -cs) REPO_TYPE=\u0026#34;ubuntu\u0026#34; else CODENAME=$(lsb_release -cs) REPO_TYPE=\u0026#34;debian\u0026#34; fi echo \u0026#34;deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \\ https://nginx.org/packages/${REPO_TYPE} ${CODENAME} nginx\u0026#34; \\ \u0026gt; /etc/apt/sources.list.d/nginx.list # 仓库优先级 echo -e \u0026#34;Package: *\\nPin: origin nginx.org\\nPin: release o=nginx\\nPin-Priority: 900\u0026#34; \\ \u0026gt; /etc/apt/preferences.d/99nginx apt-get update -y apt-get install -y nginx } install_nginx_rhel() { info \u0026#34;检测到 RHEL/CentOS/Rocky/Alma 系统 (${OS} ${OS_VERSION})\u0026#34; MAJOR_VER=$(echo \u0026#34;$OS_VERSION\u0026#34; | cut -d. -f1) info \u0026#34;添加 Nginx 官方仓库...\u0026#34; cat \u0026gt; /etc/yum.repos.d/nginx.repo \u0026lt;\u0026lt;EOF [nginx-stable] name=nginx stable repo baseurl=http://nginx.org/packages/centos/${MAJOR_VER}/\\$basearch/ gpgcheck=1 enabled=1 gpgkey=https://nginx.org/keys/nginx_signing.key module_hotfixes=true EOF info \u0026#34;安装 Nginx...\u0026#34; if command -v dnf \u0026amp;\u0026gt;/dev/null; then dnf install -y nginx else yum install -y nginx fi } # -------- 配置防火墙 -------- configure_firewall() { info \u0026#34;配置防火墙放行 80/443...\u0026#34; if command -v ufw \u0026amp;\u0026gt;/dev/null; then ufw allow \u0026#39;Nginx Full\u0026#39; 2\u0026gt;/dev/null || ufw allow 80/tcp \u0026amp;\u0026amp; ufw allow 443/tcp success \u0026#34;UFW 已放行 80/443\u0026#34; elif command -v firewall-cmd \u0026amp;\u0026gt;/dev/null; then firewall-cmd --permanent --add-service=http 2\u0026gt;/dev/null || true firewall-cmd --permanent --add-service=https 2\u0026gt;/dev/null || true firewall-cmd --reload 2\u0026gt;/dev/null || true success \u0026#34;firewalld 已放行 80/443\u0026#34; else warn \u0026#34;未检测到 ufw/firewalld，请手动放行 80/443 端口\u0026#34; fi } # -------- 启动并设置开机自启 -------- enable_nginx() { info \u0026#34;启动 Nginx 并设置开机自启...\u0026#34; systemctl enable nginx systemctl start nginx success \u0026#34;Nginx 已启动\u0026#34; } # -------- 打印结果 -------- print_result() { NGINX_VERSION=$(nginx -v 2\u0026gt;\u0026amp;1 | awk -F\u0026#39;/\u0026#39; \u0026#39;{print $2}\u0026#39;) IP=$(hostname -I | awk \u0026#39;{print $1}\u0026#39;) echo \u0026#34;\u0026#34; echo -e \u0026#34;${GREEN}================================================${NC}\u0026#34; echo -e \u0026#34;${GREEN} Nginx 安装成功！${NC}\u0026#34; echo -e \u0026#34;${GREEN}================================================${NC}\u0026#34; echo -e \u0026#34; 版本: ${CYAN}${NGINX_VERSION}${NC}\u0026#34; echo -e \u0026#34; 访问地址: ${CYAN}http://${IP}${NC}\u0026#34; echo -e \u0026#34; 配置文件: ${CYAN}/etc/nginx/nginx.conf${NC}\u0026#34; echo -e \u0026#34; 站点目录: ${CYAN}/etc/nginx/conf.d/${NC}\u0026#34; echo -e \u0026#34; 默认网站根: ${CYAN}/usr/share/nginx/html${NC}\u0026#34; echo -e \u0026#34; 日志目录: ${CYAN}/var/log/nginx/${NC}\u0026#34; echo \u0026#34;\u0026#34; echo -e \u0026#34; 常用命令:\u0026#34; echo -e \u0026#34; ${YELLOW}systemctl start|stop|restart|status nginx${NC}\u0026#34; echo -e \u0026#34; ${YELLOW}nginx -t${NC} # 检查配置语法\u0026#34; echo -e \u0026#34; ${YELLOW}nginx -s reload${NC} # 热重载配置\u0026#34; echo -e \u0026#34;${GREEN}================================================${NC}\u0026#34; } # -------- 主流程 -------- main() { echo \u0026#34;\u0026#34; echo -e \u0026#34;${CYAN}========================================${NC}\u0026#34; echo -e \u0026#34;${CYAN} Nginx 一键安装脚本 by LOCRIAN_V ${NC}\u0026#34; echo -e \u0026#34;${CYAN}========================================${NC}\u0026#34; echo \u0026#34;\u0026#34; detect_os # 检查是否已安装 if command -v nginx \u0026amp;\u0026gt;/dev/null; then warn \u0026#34;Nginx 已安装: $(nginx -v 2\u0026gt;\u0026amp;1)\u0026#34; read -p \u0026#34;是否重新安装？(y/N): \u0026#34; REINSTALL [[ \u0026#34;$REINSTALL\u0026#34; =~ ^[Yy]$ ]] || { info \u0026#34;跳过安装\u0026#34;; print_result; exit 0; } fi case \u0026#34;$OS\u0026#34; in ubuntu|debian) install_nginx_debian ;; centos|rhel|rocky|almalinux|ol) install_nginx_rhel ;; *) error \u0026#34;不支持的发行版: $OS，支持 Ubuntu/Debian/CentOS/RHEL/Rocky/Alma\u0026#34; ;; esac configure_firewall enable_nginx print_result } main \u0026#34;$@\u0026#34; 容器化与运维 (Docker) Docker 安装 (Linux) # 下载官方安装脚本 curl -fsSL https://get.docker.com -o get-docker.sh # 运行脚本安装 Docker sudo sh ./get-docker.sh Docker 备份恢复卷 # 备份 docker run --rm -v 卷名:/source -v $(pwd):/backup alpine tar czf /backup/备份文件名.tar.gz -C /source . # 恢复 # 如果 Volume 已存在，直接恢复（会覆盖原有数据） docker run --rm -v existing-volume:/restore -v $(pwd):/backup alpine tar xzf /backup/auth-backup-20240115.tar.gz -C /restore 微服务 dockercompose FROM eclipse-temurin:17-jre # 哪个服务由 compose 的 build.args 传进来 ARG MODULE ENV TZ=Asia/Shanghai WORKDIR /app COPY ${MODULE}/target/${MODULE}-0.0.1-SNAPSHOT.jar app.jar ENTRYPOINT [\u0026#34;java\u0026#34;, \u0026#34;-jar\u0026#34;, \u0026#34;app.jar\u0026#34;] services: # ===== 基础设施 ===== nacos: image: nacos/nacos-server:v2.4.3 container_name: nacos restart: always environment: MODE: standalone # 4G 预算下进一步收窄，standalone 模式够用 JVM_XMS: 512m JVM_XMX: 768m JVM_XMN: 256m ports: - \u0026#34;8848:8848\u0026#34; - \u0026#34;9848:9848\u0026#34; networks: - backend_network deploy: resources: limits: memory: 2g # ≈ JVM_XMX(256m) × 1.25，standalone 元数据量小，余量可以收紧 cpus: \u0026#34;0.5\u0026#34; mysql: image: mysql:8.0.36 container_name: mysql restart: always environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} TZ: Asia/Shanghai ports: - \u0026#34;3306:3306\u0026#34; volumes: - mysql_data:/var/lib/mysql - ./docker/mysql/mysql-init:/docker-entrypoint-initdb.d healthcheck: test: [ \u0026#34;CMD\u0026#34;, \u0026#34;mysqladmin\u0026#34;, \u0026#34;ping\u0026#34;, \u0026#34;-h\u0026#34;, \u0026#34;localhost\u0026#34;, \u0026#34;-u\u0026#34;, \u0026#34;root\u0026#34;, \u0026#34;-p${MYSQL_ROOT_PASSWORD}\u0026#34; ] interval: 10s timeout: 5s retries: 5 start_period: 30s networks: - backend_network redis: image: redis:7-alpine container_name: redis restart: always environment: - REDIS_PASSWORD=${REDIS_PASSWORD} # 4G 预算下 maxmemory 收到 128mb，chat-service 的 Pub/Sub 消息体本身很小，够用 command: redis-server --maxmemory 128mb --maxmemory-policy allkeys-lru ports: - \u0026#34;6379:6379\u0026#34; volumes: - redis_data:/data healthcheck: test: [ \u0026#34;CMD\u0026#34;, \u0026#34;redis-cli\u0026#34;, \u0026#34;ping\u0026#34; ] interval: 10s timeout: 5s retries: 5 networks: - backend_network deploy: resources: limits: memory: 192m # ≈ maxmemory(128mb) + 持久化/连接开销余量 cpus: \u0026#34;0.3\u0026#34; kafka: image: apache/kafka:3.9.0 container_name: kafka hostname: kafka ports: - \u0026#34;9092:9092\u0026#34; environment: # 单节点 KRaft，本地开发吞吐量低，堆没必要给到 512m KAFKA_HEAP_OPTS: \u0026#34;-Xms192m -Xmx256m\u0026#34; KAFKA_NODE_ID: 1 KAFKA_PROCESS_ROLES: broker,controller KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093 KAFKA_LISTENERS: PLAINTEXT://:9092,INTERNAL://:29092,CONTROLLER://:9093 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,INTERNAL://kafka:29092 KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,INTERNAL:PLAINTEXT,CONTROLLER:PLAINTEXT KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 # 单分区场景下页缓存/日志段没必要预留太多，缩小段大小减少常驻内存 KAFKA_LOG_SEGMENT_BYTES: 268435456 CLUSTER_ID: MkU3OEVBNTcwNTJENDM2Qk restart: unless-stopped volumes: - kafka_data:/var/lib/kafka/data networks: - backend_network # Kafka GUI，纯调试工具，日常不需要常驻，用 profile 隔离，需要看 topic 时再启： # docker compose --profile debug up -d kafdrop kafdrop: image: obsidiandynamics/kafdrop container_name: kafdrop profiles: [\u0026#34;debug\u0026#34;] ports: - \u0026#34;9000:9000\u0026#34; environment: KAFKA_BROKERCONNECT: \u0026#34;kafka:29092\u0026#34; SERVER_SERVLET_CONTEXTPATH: \u0026#34;/\u0026#34; restart: unless-stopped networks: - backend_network deploy: resources: limits: memory: 160m cpus: \u0026#34;0.3\u0026#34; # ===== 业务微服务 ===== # 统一约定：JAVA_OPTS 需要 Dockerfile 的 ENTRYPOINT 用 `sh -c \u0026#34;java $JAVA_OPTS -jar app.jar\u0026#34;` # 这种形式读取环境变量才会生效，如果是写死的 `java -jar app.jar` 需要先改 Dockerfile。 # -XX:+UseSerialGC + -XX:ActiveProcessorCount=1：容器只分到 \u0026lt;1 核时，G1 的多 GC 线程反而是负担 # -XX:+ExitOnOutOfMemoryError：内存不够时让容器直接重启，而不是卡死在一个不可用状态 gateway: restart: unless-stopped networks: - backend_network volumes: - ./.env:/app/.env:ro environment: JAVA_OPTS: \u0026gt;- -Xms256m -Xmx512m depends_on: mysql: { condition: service_healthy } redis: { condition: service_healthy } container_name: gateway build: { context: ., args: { MODULE: gateway } } env_file: - .env ports: - \u0026#34;8080:8080\u0026#34; auth-service: restart: unless-stopped networks: - backend_network volumes: - ./.env:/app/.env:ro environment: JAVA_OPTS: \u0026gt;- -Xms256m -Xmx512m depends_on: mysql: { condition: service_healthy } redis: { condition: service_healthy } env_file: - .env container_name: auth-service build: { context: ., args: { MODULE: auth-service } } user-service: restart: unless-stopped networks: - backend_network volumes: - ./.env:/app/.env:ro environment: JAVA_OPTS: \u0026gt;- -Xms256m -Xmx512m depends_on: mysql: { condition: service_healthy } redis: { condition: service_healthy } env_file: - .env container_name: user-service build: { context: ., args: { MODULE: user-service } } family-service: restart: unless-stopped networks: - backend_network volumes: - ./.env:/app/.env:ro environment: JAVA_OPTS: \u0026gt;- -Xms256m -Xmx512m depends_on: mysql: { condition: service_healthy } redis: { condition: service_healthy } env_file: - .env container_name: family-service build: { context: ., args: { MODULE: family-service } } chat-service: restart: unless-stopped networks: - backend_network volumes: - ./.env:/app/.env:ro environment: JAVA_OPTS: \u0026gt;- -Xms256m -Xmx512m depends_on: mysql: { condition: service_healthy } redis: { condition: service_healthy } env_file: - .env container_name: chat-service build: { context: ., args: { MODULE: chat-service } } location-service: restart: unless-stopped networks: - backend_network volumes: - ./.env:/app/.env:ro environment: JAVA_OPTS: \u0026gt;- -Xms256m -Xmx512m depends_on: mysql: { condition: service_healthy } redis: { condition: service_healthy } env_file: - .env container_name: location-service build: { context: ., args: { MODULE: location-service } } moment-service: restart: unless-stopped networks: - backend_network volumes: - ./.env:/app/.env:ro environment: JAVA_OPTS: \u0026gt;- -Xms256m -Xmx512m depends_on: mysql: { condition: service_healthy } redis: { condition: service_healthy } env_file: - .env container_name: moment-service build: { context: ., args: { MODULE: moment-service } } health-service: restart: unless-stopped networks: - backend_network volumes: - ./.env:/app/.env:ro environment: JAVA_OPTS: \u0026gt;- -Xms256m -Xmx512m depends_on: mysql: { condition: service_healthy } redis: { condition: service_healthy } env_file: - .env container_name: health-service build: { context: ., args: { MODULE: health-service } } redpacket-service: restart: unless-stopped networks: - backend_network volumes: - ./.env:/app/.env:ro environment: JAVA_OPTS: \u0026gt;- -Xms256m -Xmx512m depends_on: mysql: { condition: service_healthy } redis: { condition: service_healthy } env_file: - .env container_name: redpacket-service build: { context: ., args: { MODULE: redpacket-service } } volumes: mysql_data: redis_data: kafka_data: networks: backend_network: driver: bridge 快捷命令 # 一键停止所有容器 docker stop $(docker ps -q) MySQL 持久化数据卷创建： docker volume create mysql-data 常用启动参数提示： -d (后台运行容器) n8n (自动化工作流引擎) 持久化数据卷创建： docker volume create n8n_data 容器启动命令： docker run -it --rm \\ --name n8n \\ -p 5678:5678 \\ -e GENERIC_TIMEZONE=\u0026#34;Asia/Shanghai\u0026#34; \\ -e TZ=\u0026#34;Asia/Shanghai\u0026#34; \\ -e N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true \\ -e N8N_RUNNERS_ENABLED=true \\ -v n8n_data:/home/node/.n8n \\ docker.n8n.io/n8nio/n8n 💡 提示： 生产环境建议将 -it --rm（交互式运行且退出后删除）替换为 -d --restart always 以保障后台常驻运行。\n开发环境与语言 (Development Environment) Node.js (Linux 环境安装) 推荐使用 NVM (Node Version Manager) 进行版本管理，安装与验证步骤如下：\n# 1. 下载并安装 nvm curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.5/install.sh | bash # 2. 刷新环境变量 (免重启 Shell) \\. \u0026#34;$HOME/.nvm/nvm.sh\u0026#34; # 3. 安装指定版本的 Node.js (以 v24 为例) nvm install 24 # 4. 验证安装结果 node -v # 应输出 \u0026#34;v24.x.x\u0026#34; npm -v # 应输出对应的 npm 版本 其它常用命令 (Miscellaneous) export：用于在 Linux 中设置或导出环境变量。 神秘代码 Linux wget -P /root -N --no-check-certificate \u0026#34;https://raw.githubusercontent.com/mack-a/v2ray-agent/master/install.sh\u0026#34; \u0026amp;\u0026amp; chmod +x install.sh \u0026amp;\u0026amp; ./install.sh Windows irm https://get.activated.win | iex irm https://christitus.com/win | iex Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser; iex (irm get.scoop.sh) winget install JanDeDobbeleer.OhMyPosh -s winget oh-my-posh init pwsh | Invoke-Expression oh-my-posh font install # 展示配置 winget install Fastfetch ","permalink":"https://lv-blog.pages.dev/posts/programming/%E7%A8%8B%E5%BA%8F%E5%91%98%E5%BF%AB%E9%80%9F%E5%8F%82%E8%80%83%E5%B0%8F%E6%89%8B%E5%86%8C/","summary":"\u003cblockquote\u003e\n\u003cp\u003e有的命令一个月可能就用那么几次，不写手册里谁能记得住啊\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"vscode\"\u003eVSCode\u003c/h2\u003e\n\u003ch3 id=\"解决-code-runner-java-output-乱码\"\u003e解决 code-runner Java Output 乱码\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-json\" data-lang=\"json\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;code-runner.executorMap\u0026#34;\u003c/span\u003e\u003cspan style=\"color:#960050;background-color:#1e0010\"\u003e:\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026#34;javascript\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;node\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026#34;java\u0026#34;\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;\\\u0026#34;C:/Program Files/Java/jdk-17/bin/java.exe\\\u0026#34; -Dfile.encoding=UTF-8\u0026#34;\u003c/span\u003e,\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"powershell\"\u003ePowerShell\u003c/h2\u003e\n\u003ch3 id=\"快速完成端口转发\"\u003e快速完成端口转发\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003enetsh interface portproxy add v4tov4 \u003cspan style=\"color:#e6db74\"\u003e`\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003elistenaddress\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e0.0.0.0 \u003cspan style=\"color:#e6db74\"\u003e`\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003elistenport\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e2375\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e`\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003econnectaddress\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e127.0.0.1 \u003cspan style=\"color:#e6db74\"\u003e`\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003econnectport\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e\u003cspan style=\"color:#ae81ff\"\u003e2375\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"查端口占用-杀进程\"\u003e查端口占用 杀进程\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-Shell\" data-lang=\"Shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003enetstat -aon | findstr :8080\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003etaskkill /PID \u003cspan style=\"color:#ae81ff\"\u003e39656\u003c/span\u003e /F\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"自定义常用命令别名\"\u003e自定义常用命令别名\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003enotepad $PROFILE\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efunction\u003c/span\u003e gacp \u003cspan style=\"color:#f92672\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    param \u003cspan style=\"color:#f92672\"\u003e(\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003e[\u003c/span\u003estring\u003cspan style=\"color:#f92672\"\u003e]\u003c/span\u003e$msg \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;update\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    git add .\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    git commit -m $msg\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    git push\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#  删除当前文件夹下空文件夹\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efunction\u003c/span\u003e rme \u003cspan style=\"color:#f92672\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    Get-ChildItem -Directory -Recurse | Where-Object \u003cspan style=\"color:#f92672\"\u003e{\u003c/span\u003e $_.GetFileSystemInfos\u003cspan style=\"color:#f92672\"\u003e()\u003c/span\u003e.Count -eq \u003cspan style=\"color:#ae81ff\"\u003e0\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e}\u003c/span\u003e | Remove-Item\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"linux\"\u003eLinux\u003c/h2\u003e\n\u003ch3 id=\"linux开辟虚拟内存swap\"\u003eLinux开辟虚拟内存（Swap）\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003efree -h\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"linux实现frp内网穿透\"\u003eLinux实现FRP内网穿透\u003c/h3\u003e\n\u003cp\u003e的是让国内用户能连到一台\u003cstrong\u003e没有公网 IP 的家庭电脑\u003c/strong\u003e——用的是\u003cstrong\u003e反向内网穿透\u003c/strong\u003e:\u003c/p\u003e","title":"程序员快速参考小手册"},{"content":" Issue 来源：spring-cloud-alibaba#3931\n影响版本：spring-cloud-alibaba 2023.0.1.3+\n一、问题描述 在 Spring Cloud Alibaba 2023.X 版本中，将 Nacos 配置（包括 extension-configs、shared-configs 等）放在 bootstrap.yml / bootstrap.properties 中，配置中心的内容无法正常加载，但日志显示 bootstrap 文件本身已被读取。\n将相同配置移到 application.yml 后，一切恢复正常。\n二、根本原因 flowchart TD A[bootstrap.yml 被读取] --\u003e B{SCA 版本判断} B -- 2023.0.1.2 及以前 --\u003e C[✅ 正常加载 Nacos 配置中心] B -- 2023.0.1.3 及以后 --\u003e D[❌ extension-configs / shared-configs 失效] D --\u003e E[问题根源：SCA 2023.0.1.3 修改了配置加载优先级机制] E --\u003e F[bootstrap 阶段注册的 Nacos PropertySource 被后续流程覆盖或丢弃] 简单来说：\nSCA 2023.0.1.3 做了一次不向后兼容的内部变更，导致 bootstrap.yml 中的 Nacos 扩展配置在加载链路中被\u0026quot;丢掉\u0026quot;，而 application.yml 中的配置走的是新路径，不受影响。\n三、解决方案 ✅ 方案一（推荐）：迁移到 application.yml（官方推荐方式） SCA 2023.X 已全面转向 spring.config.import 机制，不再以 bootstrap 为主要入口。\n# application.yml spring: application: name: your-service cloud: nacos: config: server-addr: 127.0.0.1:8848 namespace: your-namespace group: DEFAULT_GROUP file-extension: yaml extension-configs: - data-id: common.yaml group: DEFAULT_GROUP refresh: true shared-configs: - data-id: shared.yaml group: DEFAULT_GROUP refresh: true config: import: - nacos:your-service.yaml ✅ 方案二：版本回退（临时方案） 如果项目暂时无法迁移配置文件，可回退到 2023.0.1.2：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-alibaba-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2023.0.1.2\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; ⚠️ 此方案仅作过渡，不建议长期使用旧版本。\n⚠️ 无效方案（已验证） 加入 spring-cloud-starter-bootstrap 依赖不能解决此问题：\n\u0026lt;!-- ❌ 加了也没用 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-bootstrap\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 原因：bootstrap.yml 本身 已经被读取（日志可见），问题出在 Nacos 扩展配置的后续处理阶段，而非 bootstrap 上下文未启动。\n四、配置加载优先级（2023.X 新机制） flowchart TD A[bootstrap.properties] --\u003e B[配置中心 Nacos] B --\u003e C[application.properties] C --\u003e D[最终生效配置] 注意：在 2023.X 的新链路下，优先级从低到高为：\nbootstrap → Nacos 配置中心 → application\n即 Nacos 配置中心的值可以覆盖 bootstrap，但会被 application 中的同名配置覆盖。\n这与旧版本行为不同，迁移时需特别注意。\n五、总结 项目 内容 影响版本 SCA 2023.0.1.3 及以上 现象 bootstrap.yml 中 Nacos 配置被读取但不生效 根因 SCA 小版本变更了配置加载机制，bootstrap 路径下的 Nacos 扩展配置被丢弃 推荐解法 全部迁移至 application.yml + spring.config.import 临时解法 回退到 2023.0.1.2 无效操作 添加 spring-cloud-starter-bootstrap 依赖 ","permalink":"https://lv-blog.pages.dev/posts/programming/backend/bugs-solutions/sca-nacos-bootstrap-bug/","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eIssue 来源\u003c/strong\u003e：\u003ca href=\"https://github.com/alibaba/spring-cloud-alibaba/issues/3931\"\u003espring-cloud-alibaba#3931\u003c/a\u003e\u003cbr\u003e\n\u003cstrong\u003e影响版本\u003c/strong\u003e：\u003ccode\u003espring-cloud-alibaba 2023.0.1.3+\u003c/code\u003e\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"一问题描述\"\u003e一、问题描述\u003c/h2\u003e\n\u003cp\u003e在 Spring Cloud Alibaba 2023.X 版本中，将 Nacos 配置（包括 \u003ccode\u003eextension-configs\u003c/code\u003e、\u003ccode\u003eshared-configs\u003c/code\u003e 等）放在 \u003ccode\u003ebootstrap.yml\u003c/code\u003e / \u003ccode\u003ebootstrap.properties\u003c/code\u003e 中，\u003cstrong\u003e配置中心的内容无法正常加载\u003c/strong\u003e，但日志显示 \u003ccode\u003ebootstrap\u003c/code\u003e 文件本身已被读取。\u003c/p\u003e\n\u003cp\u003e将相同配置移到 \u003ccode\u003eapplication.yml\u003c/code\u003e 后，一切恢复正常。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"二根本原因\"\u003e二、根本原因\u003c/h2\u003e\n\u003cpre class=\"mermaid\"\u003eflowchart TD\n    A[bootstrap.yml 被读取] --\u003e B{SCA 版本判断}\n    B -- 2023.0.1.2 及以前 --\u003e C[✅ 正常加载 Nacos 配置中心]\n    B -- 2023.0.1.3 及以后 --\u003e D[❌ extension-configs / shared-configs 失效]\n    D --\u003e E[问题根源：SCA 2023.0.1.3 修改了配置加载优先级机制]\n    E --\u003e F[bootstrap 阶段注册的 Nacos PropertySource 被后续流程覆盖或丢弃]\n\u003c/pre\u003e\n\u003cp\u003e\u003cstrong\u003e简单来说：\u003c/strong\u003e\u003cbr\u003e\nSCA \u003ccode\u003e2023.0.1.3\u003c/code\u003e 做了一次\u003cstrong\u003e不向后兼容的内部变更\u003c/strong\u003e，导致 \u003ccode\u003ebootstrap.yml\u003c/code\u003e 中的 Nacos 扩展配置在加载链路中被\u0026quot;丢掉\u0026quot;，而 \u003ccode\u003eapplication.yml\u003c/code\u003e 中的配置走的是新路径，不受影响。\u003c/p\u003e","title":"SCA 2023.X — Nacos Bootstrap 配置失效问题排查与解决方案"},{"content":"bootstrap 配置 bootstrap.yaml server: port: 10888 tomcat: uri-encoding: UTF-8 spring: profiles: active: dev application: name: archive-service cloud: nacos: config: file-extension: yaml shared-configs: - data-id: shared-spring.yaml refresh: false - data-id: shared-redis.yaml refresh: false - data-id: shared-mybatis.yaml refresh: false - data-id: shared-logs.yaml refresh: false - data-id: shared-feign.yaml refresh: false - data-id: shared-logs.yaml # 共享日志配置 refresh: false # - data-id: shared-feign.yaml # 共享feign配置 # refresh: false lia: jdbc: database: tb_archive bootstrap-dev.yaml spring: cloud: nacos: server-addr: 192.168.2.115:8848 # nacos注册中心 discovery: namespace: f923fb34-cb0a-4c06-8fca-ad61ea61a3f0 group: DEFAULT_GROUP ip: 192.168.2.115 logging: level: asia.liminality: debug nacos 配置 shared-spring.yaml spring: jackson: default-property-inclusion: non_null main: allow-bean-definition-overriding: true mvc: pathmatch: #解决异常：swagger Failed to start bean \u0026#39;documentationPluginsBootstrapper\u0026#39;; nested exception is java.lang.NullPointerException #因为Springfox使用的路径匹配是基于AntPathMatcher的，而Spring Boot 2.6.X使用的是PathPatternMatcher matching-strategy: ant_path_matcher shared-redis.yaml spring: redis: host: ${lia.redis.host:192.168.2.115} password: ${lia.redis.password:github} lettuce: pool: max-active: ${lia.redis.pool.max-active:8} max-idle: ${lia.redis.pool.max-idle:8} min-idle: ${lia.redis.pool.min-idle:1} max-wait: ${lia.redis.pool.max-wait:300} shared-mybatis.yaml mySQL spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://${sh.jdbc.host:127.0.0.1}/${sh.jdbc.database}?useUnicode=true\u0026amp;characterEncoding=utf8\u0026amp;serverTimezone=Asia/Shanghai\u0026amp;useSSL=false username: ${lia.jdbc.username:root} password: ${lia.jdbc.password:794211} mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler global-config: db-config: logic-delete-field: deletedAt logic-not-delete-value: \u0026#34;null\u0026#34; logic-delete-value: \u0026#34;now()\u0026#34; id-type: assign_id insert-strategy: not_null update-strategy: not_null porstgreSQL spring: datasource: driver-class-name: org.postgresql.Driver url: jdbc:postgresql://${lia.jdbc.host:192.168.2.115}:${lia.jdbc.port:5432}/${lia.jdbc.database}?useUnicode=true\u0026amp;characterEncoding=UTF-8\u0026amp;autoReconnect=true\u0026amp;serverTimezone=Asia/Shanghai username: ${lia.jdbc.username:postgres} password: ${lia.jdbc.password:github} mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler global-config: db-config: logic-delete-field: deletedAt logic-not-delete-value: \u0026#34;null\u0026#34; logic-delete-value: \u0026#34;now()\u0026#34; id-type: assign_id insert-strategy: ignored update-strategy: ignored select-strategy: not_null shared-logs.yaml logging: pattern: dateformat: HH:mm:ss.SSS console: \u0026#34;%clr(%d{${LOG_DATEFORMAT_PATTERN}}){faint}-[${hostname}][%X{requestId:-sys}] %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n\u0026#34; file: \u0026#34;%d{${LOG_DATEFORMAT_PATTERN}}-[${hostname}][%X{requestId:-sys}]-${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%15.15t] %-40.40logger{39} : %m%n\u0026#34; file: path: \u0026#34;logs/${spring.application.name}\u0026#34; shared-feign.yaml feign: client: config: default: # default全局的配置 loggerLevel: BASIC # 日志级别，BASIC就是基本的请求和响应信息 httpclient: enabled: true # 开启feign对HttpClient的支持 max-connections: 200 # 最大的连接数 max-connections-per-route: 50 # 每个路径的最大连接数 sentinel: enabled: true ","permalink":"https://lv-blog.pages.dev/posts/programming/backend/code-snippets/nacos-config-tips/","summary":"\u003ch1 id=\"bootstrap-配置\"\u003ebootstrap 配置\u003c/h1\u003e\n\u003ch2 id=\"bootstrapyaml\"\u003ebootstrap.yaml\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003eserver\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003eport\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e10888\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003etomcat\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003euri-encoding\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eUTF-8\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003espring\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003eprofiles\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003eactive\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003edev\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003eapplication\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003ename\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003earchive-service\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003ecloud\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003enacos\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003econfig\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003efile-extension\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eyaml\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003eshared-configs\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e          - \u003cspan style=\"color:#f92672\"\u003edata-id\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eshared-spring.yaml\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003erefresh\u003c/span\u003e: \u003cspan style=\"color:#66d9ef\"\u003efalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e          - \u003cspan style=\"color:#f92672\"\u003edata-id\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eshared-redis.yaml\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003erefresh\u003c/span\u003e: \u003cspan style=\"color:#66d9ef\"\u003efalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e          - \u003cspan style=\"color:#f92672\"\u003edata-id\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eshared-mybatis.yaml\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003erefresh\u003c/span\u003e: \u003cspan style=\"color:#66d9ef\"\u003efalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e          - \u003cspan style=\"color:#f92672\"\u003edata-id\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eshared-logs.yaml\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003erefresh\u003c/span\u003e: \u003cspan style=\"color:#66d9ef\"\u003efalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e          - \u003cspan style=\"color:#f92672\"\u003edata-id\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eshared-feign.yaml\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003erefresh\u003c/span\u003e: \u003cspan style=\"color:#66d9ef\"\u003efalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e          - \u003cspan style=\"color:#f92672\"\u003edata-id\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eshared-logs.yaml\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e# 共享日志配置\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e            \u003cspan style=\"color:#f92672\"\u003erefresh\u003c/span\u003e: \u003cspan style=\"color:#66d9ef\"\u003efalse\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#          - data-id: shared-feign.yaml # 共享feign配置\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e#            refresh: false\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003elia\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003ejdbc\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003edatabase\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003etb_archive\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"bootstrap-devyaml\"\u003ebootstrap-dev.yaml\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003espring\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003ecloud\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003enacos\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003eserver-addr\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e192.168.2.115\u003c/span\u003e:\u003cspan style=\"color:#ae81ff\"\u003e8848\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e# nacos注册中心\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003ediscovery\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003enamespace\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003ef923fb34-cb0a-4c06-8fca-ad61ea61a3f0\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003egroup\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eDEFAULT_GROUP\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003eip\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e192.168.2.115\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003elogging\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003elevel\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003easia.liminality\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003edebug\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch1 id=\"nacos-配置\"\u003enacos 配置\u003c/h1\u003e\n\u003ch2 id=\"shared-springyaml\"\u003eshared-spring.yaml\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003espring\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003ejackson\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003edefault-property-inclusion\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003enon_null\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003emain\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003eallow-bean-definition-overriding\u003c/span\u003e: \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003emvc\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003epathmatch\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#75715e\"\u003e#解决异常：swagger Failed to start bean \u0026#39;documentationPluginsBootstrapper\u0026#39;; nested exception is java.lang.NullPointerException\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#75715e\"\u003e#因为Springfox使用的路径匹配是基于AntPathMatcher的，而Spring Boot 2.6.X使用的是PathPatternMatcher\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003ematching-strategy\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eant_path_matcher\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"shared-redisyaml\"\u003eshared-redis.yaml\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003espring\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003eredis\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003ehost\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e${lia.redis.host:192.168.2.115}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003epassword\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e${lia.redis.password:github}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003elettuce\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003epool\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003emax-active\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e${lia.redis.pool.max-active:8}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003emax-idle\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e${lia.redis.pool.max-idle:8}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003emin-idle\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e${lia.redis.pool.min-idle:1}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003emax-wait\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e${lia.redis.pool.max-wait:300}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"shared-mybatisyaml\"\u003eshared-mybatis.yaml\u003c/h2\u003e\n\u003ch3 id=\"mysql\"\u003emySQL\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003espring\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003edatasource\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003edriver-class-name\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003ecom.mysql.cj.jdbc.Driver\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003eurl\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003ejdbc:mysql://${sh.jdbc.host:127.0.0.1}/${sh.jdbc.database}?useUnicode=true\u0026amp;characterEncoding=utf8\u0026amp;serverTimezone=Asia/Shanghai\u0026amp;useSSL=false\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003eusername\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e${lia.jdbc.username:root}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003epassword\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e${lia.jdbc.password:794211}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003emybatis-plus\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003econfiguration\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003edefault-enum-type-handler\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003ecom.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003eglobal-config\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003edb-config\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003elogic-delete-field\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003edeletedAt\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003elogic-not-delete-value\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;null\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003elogic-delete-value\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;now()\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003eid-type\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eassign_id\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003einsert-strategy\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003enot_null\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003eupdate-strategy\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003enot_null\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"porstgresql\"\u003eporstgreSQL\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003espring\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003edatasource\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003edriver-class-name\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eorg.postgresql.Driver\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003eurl\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003ejdbc:postgresql://${lia.jdbc.host:192.168.2.115}:${lia.jdbc.port:5432}/${lia.jdbc.database}?useUnicode=true\u0026amp;characterEncoding=UTF-8\u0026amp;autoReconnect=true\u0026amp;serverTimezone=Asia/Shanghai\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003eusername\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e${lia.jdbc.username:postgres}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003epassword\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e${lia.jdbc.password:github}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003emybatis-plus\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003econfiguration\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003edefault-enum-type-handler\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003ecom.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003eglobal-config\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003edb-config\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003elogic-delete-field\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003edeletedAt\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003elogic-not-delete-value\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;null\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003elogic-delete-value\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;now()\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003eid-type\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eassign_id\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003einsert-strategy\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eignored\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003eupdate-strategy\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eignored\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003eselect-strategy\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003enot_null\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"shared-logsyaml\"\u003eshared-logs.yaml\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003elogging\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003epattern\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003edateformat\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eHH:mm:ss.SSS\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003econsole\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;%clr(%d{${LOG_DATEFORMAT_PATTERN}}){faint}-[${hostname}][%X{requestId:-sys}] %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003efile\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;%d{${LOG_DATEFORMAT_PATTERN}}-[${hostname}][%X{requestId:-sys}]-${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%15.15t] %-40.40logger{39} : %m%n\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003efile\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003epath\u003c/span\u003e: \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;logs/${spring.application.name}\u0026#34;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"shared-feignyaml\"\u003eshared-feign.yaml\u003c/h2\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-yaml\" data-lang=\"yaml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003efeign\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003eclient\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003econfig\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e      \u003cspan style=\"color:#f92672\"\u003edefault\u003c/span\u003e: \u003cspan style=\"color:#75715e\"\u003e# default全局的配置\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#f92672\"\u003eloggerLevel\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003eBASIC\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e# 日志级别，BASIC就是基本的请求和响应信息\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003ehttpclient\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003eenabled\u003c/span\u003e: \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e# 开启feign对HttpClient的支持\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003emax-connections\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e200\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e# 最大的连接数\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003emax-connections-per-route\u003c/span\u003e: \u003cspan style=\"color:#ae81ff\"\u003e50\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e# 每个路径的最大连接数\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  \u003cspan style=\"color:#f92672\"\u003esentinel\u003c/span\u003e:\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003eenabled\u003c/span\u003e: \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"Nacos 一些常用配置"},{"content":"自动插件（不维护） 此处粘贴Wistia播放器右键获取的视频链接：\n解析视频 手动方法 在正在播放的视频上右键 选择“复制链接（Copy link）”。\n从链接里找视频 ID 你会看到类似：\nwvideo=tra6gsm6rl 这里的 tra6gsm6rl 就是视频的 ID。\n如果链接里没有，也可以： 打开网页源代码（view source），搜索：\nhashedId=tra6gsm6rl 打开嵌入页面 在浏览器里访问：\nhttp://fast.wistia.net/embed/iframe/ + 视频ID 比如：\nhttp://fast.wistia.net/embed/iframe/tra6gsm6rl 找真实视频文件地址 在打开的页面源代码里搜索：\n优先找：\n\u0026#34;type\u0026#34;:\u0026#34;original\u0026#34; 然后往下看一行，会有类似：\n\u0026#34;url\u0026#34;:\u0026#34;http://embed.wistia.com/deliveries/xxxxx.bin\u0026#34; 如果没有 original，就找：\n\u0026#34;type\u0026#34;:\u0026#34;hd_mp4_video\u0026#34; 下载视频 把找到的链接复制出来，把后缀从：\n.bin 改成：\n.mp4 然后直接打开或下载就行。\n转载自：https://gist.github.com/szepeviktor/2a8a3ce8b32e2a67ca416ffd077553c5\n","permalink":"https://lv-blog.pages.dev/posts/programming/some-tricks/how-to-download-wistia-videos/","summary":"\u003ch2 id=\"自动插件不维护\"\u003e自动插件（不维护）\u003c/h2\u003e\n\u003cp\u003e此处粘贴Wistia播放器右键获取的视频链接：\u003c/p\u003e\n\u003ctextarea id=\"input\"\u003e\u003c/textarea\u003e\n\u003cbr\u003e\n\u003cbutton onclick=\"parseVideo()\"\u003e解析视频\u003c/button\u003e\n\u003cdiv class=\"result\" id=\"output\"\u003e\u003c/div\u003e\n\u003ch2 id=\"手动方法\"\u003e手动方法\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e在正在播放的视频上右键\u003c/strong\u003e\n选择“复制链接（Copy link）”。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e从链接里找视频 ID\u003c/strong\u003e\n你会看到类似：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ewvideo=tra6gsm6rl\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e这里的 \u003ccode\u003etra6gsm6rl\u003c/code\u003e 就是视频的 ID。\u003c/p\u003e\n\u003cp\u003e如果链接里没有，也可以：\n打开网页源代码（view source），搜索：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ehashedId=tra6gsm6rl\n\u003c/code\u003e\u003c/pre\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e打开嵌入页面\u003c/strong\u003e\n在浏览器里访问：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ehttp://fast.wistia.net/embed/iframe/ + 视频ID\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e比如：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003ehttp://fast.wistia.net/embed/iframe/tra6gsm6rl\n\u003c/code\u003e\u003c/pre\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e找真实视频文件地址\u003c/strong\u003e\n在打开的页面源代码里搜索：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e优先找：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026#34;type\u0026#34;:\u0026#34;original\u0026#34;\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e然后往下看一行，会有类似：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026#34;url\u0026#34;:\u0026#34;http://embed.wistia.com/deliveries/xxxxx.bin\u0026#34;\n\u003c/code\u003e\u003c/pre\u003e\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e如果没有 original，就找：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e\u0026#34;type\u0026#34;:\u0026#34;hd_mp4_video\u0026#34;\n\u003c/code\u003e\u003c/pre\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003e下载视频\u003c/strong\u003e\n把找到的链接复制出来，把后缀从：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e.bin\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e改成：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e.mp4\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e然后直接打开或下载就行。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ol\u003e\n\u003cp\u003e转载自：https://gist.github.com/szepeviktor/2a8a3ce8b32e2a67ca416ffd077553c5\u003c/p\u003e\n\u003cstyle\u003e\n   textarea {\n  width: 100%;\n  height: 140px;\n  padding: 10px;\n  font-size: 14px;\n  box-sizing: border-box;\n  border: 1.5px solid #d0cfc8;\n  border-radius: 8px;\n  background: #fff;\n  color: #1a1a1a;\n  font-family: inherit;\n  resize: vertical;\n  outline: none;\n  transition: border-color 0.2s;\n  line-height: 1.6;\n}\n\ntextarea::placeholder {\n  color: #aaa;\n}\n\ntextarea:hover {\n  border-color: #b0afa8;\n}\n\ntextarea:focus {\n  border-color: #534AB7;\n  box-shadow: 0 0 0 3px rgba(83, 74, 183, 0.12);\n}\n\n.md-content button {\n  margin-top: 10px;\n  padding: 10px 20px;\n  cursor: pointer;\n  font-size: 14px;\n  font-family: inherit;\n  font-weight: 500;\n  border: 1.5px solid #d0cfc8;\n  border-radius: 8px;\n  background: #fff;\n  color: #1a1a1a;\n  transition: background 0.15s, border-color 0.15s, transform 0.1s;\n  outline: none;\n}\n\n.md-content button:hover {\n  background: #f5f4f0;\n  border-color: #999;\n}\n\n.md-content button:active {\n  transform: scale(0.98);\n  background: #eeede8;\n}\n\n.md-content button:focus-visible {\n  box-shadow: 0 0 0 3px rgba(83, 74, 183, 0.18);\n  border-color: #534AB7;\n}\n\u003c/style\u003e\n\u003cscript\u003e\nasync function parseVideo() {\n  const input = document.getElementById(\"input\").value;\n  const output = document.getElementById(\"output\");\n\n  function extractVideoId(text) {\n    let m = text.match(/wvideo=([a-zA-Z0-9]+)/);\n    if (m) return m[1];\n\n    m = text.match(/hashedId=([a-zA-Z0-9]+)/);\n    if (m) return m[1];\n\n    return null;\n  }\n\n  function extractVideoUrl(html) {\n    let m = html.match(/\"type\":\"original\".*?\"url\":\"(https?:\\/\\/[^\"]+)\"/);\n    if (m) return m[1].replace(\".bin\", \".mp4\");\n\n    m = html.match(/\"type\":\"hd_mp4_video\".*?\"url\":\"(https?:\\/\\/[^\"]+)\"/);\n    if (m) return m[1].replace(\".bin\", \".mp4\");\n\n    return null;\n  }\n\n  output.innerHTML = \"⏳ 解析中...\";\n\n  const videoId = extractVideoId(input);\n\n  if (!videoId) {\n    output.innerHTML = \"❌ 没找到 wvideo 或 hashedId\";\n    return;\n  }\n\n  const embedUrl = `https://fast.wistia.net/embed/iframe/${videoId}`;\n\n  try {\n    const res = await fetch(embedUrl);\n    const html = await res.text();\n\n    const videoUrl = extractVideoUrl(html);\n\n    if (!videoUrl) {\n      output.innerHTML = \"❌ 没找到视频直链\";\n      return;\n    }\n\n    output.innerHTML = `\n      \u003cdiv\u003e\u003cspan class=\"label\"\u003eVideo ID：\u003c/span\u003e${videoId}\u003c/div\u003e\n      \u003cdiv style=\"margin-top:10px\"\u003e\u003cspan class=\"label\"\u003eMP4 直链：\u003c/span\u003e\u003c/div\u003e\n      \u003cdiv\u003e${videoUrl}\u003c/div\u003e\n      \u003cbr\u003e\n      \u003cbutton onclick=\"navigator.clipboard.writeText('${videoUrl}')\"\u003e📋 复制链接\u003c/button\u003e\n    `;\n\n  } catch (e) {\n    output.innerHTML = \"❌ 请求失败：\" + e.message;\n  }\n}\n\u003c/script\u003e","title":"Wistia视频下载方法"},{"content":"微信小程序和后端用户认证交互流程 微信小程序登录的核心流程：小程序拿临时凭证 code → 后端用 code 换 openid → 后端发自己的 JWT 给小程序。下面是整体流程图。\nsequenceDiagram participant MP as 微信小程序 participant WX as 微信服务器 participant GW as gateway participant Auth as auth模块 participant User as user-service participant DB as lia_user库 MP-\u003e\u003eWX: wx.login() 获取 code WX--\u003e\u003eMP: 返回临时 code MP-\u003e\u003eGW: POST /auth/wx-login {code} GW-\u003e\u003eAuth: 转发请求 Auth-\u003e\u003eWX: code2session(code) WX--\u003e\u003eAuth: 返回 openid + session_key Auth-\u003e\u003eUser: OpenFeign 查/建用户 User-\u003e\u003eDB: 按 openid 查询 DB--\u003e\u003eUser: 用户记录(或空) User--\u003e\u003eAuth: 返回 userId Auth-\u003e\u003eAuth: 生成 JWT Auth--\u003e\u003eMP: 返回 token + 用户信息 MP-\u003e\u003eMP: 存入 Storage 关键点拆解 1. 小程序端拿 code（前端）\nwx.login({ success(res) { // res.code 是一次性临时凭证，5分钟过期 wx.request({ url: \u0026#39;https://你的域名/auth/wx-login\u0026#39;, method: \u0026#39;POST\u0026#39;, data: { code: res.code } }) } }) 2. 后端用 code 换 openid\n请求微信接口 https://api.weixin.qq.com/sns/jscode2session，带四个参数：\n参数 值 appid 小程序 AppID secret 小程序 AppSecret（配在 Nacos，别写死） js_code 前端传来的 code grant_type 固定填 authorization_code 微信返回 openid（用户唯一标识）和 session_key（解密用，登录场景可暂时不用）。\n3. openid 是身份，不是密码\nopenid 只是\u0026quot;这个用户是谁\u0026quot;的标识。你不能信任前端直接传 openid，必须每次都用 code 去微信换，这样才防伪造。\n4. 在你的架构里怎么放\n放 auth 模块最合适，和你已有的 JWT 登录逻辑并列：\nauth 模块新增 WxLoginController + WxLoginService 调微信接口用 RestClient/WebClient（你已在用 Spring Boot 3） 拿到 openid 后，通过 OpenFeign 调 user-service 做\u0026quot;查不到就注册\u0026quot;（upsert） lia_user 表加一列 openid（唯一索引） 复用你现有的 JWT 工具类签发 token 5. 数据库改动\nALTER TABLE tb_user ADD COLUMN openid VARCHAR(64) UNIQUE; 整个流程和你现在的账号密码登录共用同一套 JWT 体系，只是\u0026quot;验证身份\u0026quot;那一步从\u0026quot;校验密码\u0026quot;换成了\u0026quot;用 code 换 openid\u0026quot;。\n需要我帮你写 auth 模块那段调微信接口 + 签发 JWT 的完整代码吗？\n","permalink":"https://lv-blog.pages.dev/posts/programming/wechatapp-development-notes/","summary":"\u003ch2 id=\"微信小程序和后端用户认证交互流程\"\u003e微信小程序和后端用户认证交互流程\u003c/h2\u003e\n\u003cp\u003e微信小程序登录的核心流程：小程序拿临时凭证 \u003ccode\u003ecode\u003c/code\u003e → 后端用 \u003ccode\u003ecode\u003c/code\u003e 换 \u003ccode\u003eopenid\u003c/code\u003e → 后端发自己的 JWT 给小程序。下面是整体流程图。\u003c/p\u003e\n\u003cpre class=\"mermaid\"\u003esequenceDiagram\n    participant MP as 微信小程序\n    participant WX as 微信服务器\n    participant GW as gateway\n    participant Auth as auth模块\n    participant User as user-service\n    participant DB as lia_user库\n\n    MP-\u003e\u003eWX: wx.login() 获取 code\n    WX--\u003e\u003eMP: 返回临时 code\n    MP-\u003e\u003eGW: POST /auth/wx-login {code}\n    GW-\u003e\u003eAuth: 转发请求\n    Auth-\u003e\u003eWX: code2session(code)\n    WX--\u003e\u003eAuth: 返回 openid + session_key\n    Auth-\u003e\u003eUser: OpenFeign 查/建用户\n    User-\u003e\u003eDB: 按 openid 查询\n    DB--\u003e\u003eUser: 用户记录(或空)\n    User--\u003e\u003eAuth: 返回 userId\n    Auth-\u003e\u003eAuth: 生成 JWT\n    Auth--\u003e\u003eMP: 返回 token + 用户信息\n    MP-\u003e\u003eMP: 存入 Storage\n\u003c/pre\u003e\n\u003ch3 id=\"关键点拆解\"\u003e关键点拆解\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e1. 小程序端拿 code（前端）\u003c/strong\u003e\u003c/p\u003e","title":"微信小程序开发受苦指南"},{"content":"消息队列通识 为什么需要消息队列 解耦 削峰填谷 异步调用 消息队列如何保持幂等 重复源泉 生产端重发 发送ACK没有回复继续发送 消费端重复 解决方案 唯一消息ID，生产者发送时携带messageId 幂等表 状态机校验 乐观锁 / 版本号 分布式锁 核心原则 消费逻辑天然幂等 为什么需要消息队列 解耦 异步 削峰 消息队列的模型有哪些 点对点模型 发布订阅模型 如何保证消息不丢失 生产端 ack broker 持久化 生产环境副本因子 \u0026gt;= 3 消费端 手动提交 offset 如何保证消息有序 让需要保持有序的消息落到同一个分区 给唯一的key，且key以后不会重复 Kafka保证唯一分区内有序，跨分区无序 RabbitMQ RabbitMQ 如何保证消息不丢失 哪些情况会丢失 publisher -\u0026gt; exchange 生产者确认机制 ack publish-confirm nack publish-confirm ack publish-return 消息失败之后如何处理 回调方法 记录日志 保存到数据库中然后定时重发 exchange -\u0026gt; queue 消息持久化 交换机持久化 队列持久化 消息持久化 queue queue -\u0026gt; consumer 消费者确认机制 RabbitMQ 消息的重复消费问题是如何解决的 重复消费的原因\n网络抖动 消费者挂了 解决方案\n每条消息设置一个唯一的标识id 幂等方案 分布式锁 数据库锁 悲观锁 乐观锁 RabbitMQ 中的死信交换机（延迟队列） 延迟队列 = 死信交换机\n消息堆积问题 增加消费者 消费者端开启线程池 扩大队列容积 惰性队列 RabbitMQ 的高可用机制 普通集群 镜像集群 仲裁队列 Kafka Kafka 如何保证消息不丢失？ 丢失的三个情况 producer发送方式 同步 get 异步 解决生产者丢失 设置异步发送 消息重试 解决broker存储中丢失 发送确认机制 acks 0 1 all 解决消费者主从 broker 接收消息丢失 禁止自动提交偏移量，改为手动提交偏移量 同步提交 异步提交 同步+异步组合提交 如何解决Kafka的消息重复消费问题 关闭自动提交 同步+异步提交 幂等方案 Kafka 如何保证消费的顺序性 指定分区号 发送消息时按照相同的业务设置相同的key Kafka 高可用机制了解过吗 集群 分区备份机制 ISR副本（In-Sync Replica） 普通副本 解释一下复制机制中的ISR？ 分区副本的follwer分为两类 一类是ISR与leader副本同步保存数据 另一个是普通副本异步保存数据\nKafka 数据清理机制了解过吗？ Kafka 文件存储机制 分段存储 .index .log .timeindex 分段的好处 减少单个文件内容大小，查找数据方便 方便kafka进行日志清理 数据清理机制 日志的清理策略 根据消息保留时间，如果消息超过指定时间，触发清理 默认7天 根据topic存储的数据大小，日志文件大小超过一定阈值进行清理（默认关闭） Kafka 中实现高性能的设计有了解过吗？ 多性能是多方面协同的结果 主要体现 消息分区 顺序读写 页缓存 Linux系统的缓存。将磁盘数据缓存到内存，提高性能 零拷贝 减少上下文切换以及数据拷贝 消息压缩 提供多种消息压缩算法，减少磁盘和网络IO 压缩耗费CPU 分批发送 将消息打包分批发送，减少网络开销 零拷贝的含义 用户空间 kafka 内核空间 页缓存 Socket缓冲区 硬件 磁盘文件 网卡\n节省了页缓存到网卡的中间开销\n页缓存直接到网卡\nZooKeeper 的作用，为什么抛弃？ 作用 broker 注册 topic / partition 元数据 controller 选举 ISR 变更 抛弃原因 运维两套系统麻烦 ZK 成为元数据规模的瓶颈 ZK 为了保证高性能，将整个数据树完全保存在内存中 Kafka能管理的分区总数，受限于 ZK 服务器的物理内存 故障恢复慢 Leader 宕机或集群重启时，必须完成一套严格的恢复流程 Kafka 为什么这么快 顺序IO写磁盘 分区并行 批量发送 零拷贝 数据在操作系统内核空间直接传递 避免内核空间和用户空间之间多次拷贝和上下文切换 Kafka 的零拷贝是指利用 Linux 的 sendfile 系统调用，让数据从磁盘 PageCache 直接发送到网卡，绕过了用户态内存拷贝，从而极大降低了 CPU 开销并提升了消费吞吐量 ","permalink":"https://lv-blog.pages.dev/posts/programming/for-interview/mq-interview-questions/","summary":"\u003ch1 id=\"消息队列通识\"\u003e消息队列通识\u003c/h1\u003e\n\u003ch2 id=\"为什么需要消息队列\"\u003e为什么需要消息队列\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e解耦\u003c/li\u003e\n\u003cli\u003e削峰填谷\u003c/li\u003e\n\u003cli\u003e异步调用\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch2 id=\"消息队列如何保持幂等\"\u003e消息队列如何保持幂等\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e重复源泉\n\u003cul\u003e\n\u003cli\u003e生产端重发\n\u003cul\u003e\n\u003cli\u003e发送ACK没有回复继续发送\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e消费端重复\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e解决方案\n\u003cul\u003e\n\u003cli\u003e唯一消息ID，生产者发送时携带messageId\u003c/li\u003e\n\u003cli\u003e幂等表\u003c/li\u003e\n\u003cli\u003e状态机校验\u003c/li\u003e\n\u003cli\u003e乐观锁 / 版本号\u003c/li\u003e\n\u003cli\u003e分布式锁\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e核心原则\n\u003cul\u003e\n\u003cli\u003e消费逻辑天然幂等\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"为什么需要消息队列-1\"\u003e为什么需要消息队列\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e解耦\u003c/li\u003e\n\u003cli\u003e异步\u003c/li\u003e\n\u003cli\u003e削峰\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"消息队列的模型有哪些\"\u003e消息队列的模型有哪些\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e点对点模型\u003c/li\u003e\n\u003cli\u003e发布订阅模型\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"如何保证消息不丢失\"\u003e如何保证消息不丢失\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e生产端\n\u003cul\u003e\n\u003cli\u003eack\u003c/li\u003e\n\u003cli\u003ebroker 持久化\n\u003cul\u003e\n\u003cli\u003e生产环境副本因子 \u0026gt;= 3\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e消费端\n\u003cul\u003e\n\u003cli\u003e手动提交 offset\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"如何保证消息有序\"\u003e如何保证消息有序\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e让需要保持有序的消息落到同一个分区\u003c/li\u003e\n\u003cli\u003e给唯一的key，且key以后不会重复\u003c/li\u003e\n\u003cli\u003eKafka保证唯一分区内有序，跨分区无序\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"rabbitmq\"\u003eRabbitMQ\u003c/h1\u003e\n\u003ch2 id=\"rabbitmq-如何保证消息不丢失\"\u003eRabbitMQ 如何保证消息不丢失\u003c/h2\u003e\n\u003c!-- TODO 复习 --\u003e\n\u003cul\u003e\n\u003cli\u003e哪些情况会丢失\n\u003cul\u003e\n\u003cli\u003epublisher -\u0026gt; exchange\n\u003cul\u003e\n\u003cli\u003e生产者确认机制\n\u003cul\u003e\n\u003cli\u003eack publish-confirm\u003c/li\u003e\n\u003cli\u003enack publish-confirm\u003c/li\u003e\n\u003cli\u003eack publish-return\u003c/li\u003e\n\u003cli\u003e消息失败之后如何处理\n\u003cul\u003e\n\u003cli\u003e回调方法\u003c/li\u003e\n\u003cli\u003e记录日志\u003c/li\u003e\n\u003cli\u003e保存到数据库中然后定时重发\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003eexchange -\u0026gt; queue\n\u003cul\u003e\n\u003cli\u003e消息持久化\n\u003cul\u003e\n\u003cli\u003e交换机持久化\u003c/li\u003e\n\u003cli\u003e队列持久化\u003c/li\u003e\n\u003cli\u003e消息持久化\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003equeue\u003c/li\u003e\n\u003cli\u003equeue -\u0026gt; consumer\n\u003cul\u003e\n\u003cli\u003e消费者确认机制\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"rabbitmq-消息的重复消费问题是如何解决的\"\u003eRabbitMQ 消息的重复消费问题是如何解决的\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e重复消费的原因\u003c/strong\u003e\u003c/p\u003e","title":"消息中间件 面试题整理"},{"content":"SpringCloud 问题 springcloud 五大组件 Eureka / nacos Ribbon Feign / dubbo Hystrix / sentinel Zuul/Gateway 服务注册和发现是什么意思，SpringCloud 如何实现服务注册和发现？ nacos 和 eureka 的区别？ 相同点 服务提供者 注册 服务信息 到 注册中心 服务消费者 定时从 注册中心 拉取 服务消费者 对 服务提供者 进行远程调用 服务提供者 临时实例 采用心跳检查 什么是临时实例？ 默认实例都是临时实例 临时实例心跳模式 非临时主动探测 临时实例没有心跳会被剔除 非临时不会被剔除 ephemeral: false 变成非临时实例 非临时实例，nacos会主动询问服务提供者是否存活 不同点 非临时实例 nacos集群AP，如果存在非临时实例，采用CP eureka采用AP nacos还有配置中心 你们项目中负载均衡是如何实现的？ Ribbon Ribbon 负载均衡流程 微服务发起请求 Ribbon 从注册中心拉取 根据策略进行转发 Ribbon 负载均衡策略有哪些？ RoundRobinRule 简单轮询 WeightedResponseTimeRule 按照权重来选择服务器，响应时间越长，权重越小 RandomRule BestAvailableRule 忽略短路的服务器，选择并发数较低的服务器 RetryRule 重试机制的选择逻辑（基于轮询） AvailabilityFilteringRule 可用性敏感策略。先过滤非健康的，再选择连接数比较小的 ZoneAvoidanceRule 以区域可用的服务器为基础对服务器选择。 如果想自定义负载均衡策略，如何实现？ 自己创建类实现 IRule 接口， 再通过配置类或配置文件配置 @Bean IRule 全局生效 NFLoadBalancerRuleClassName 局部生效 什么是服务雪崩？如何解决这个问题？ 服务雪崩 一个服务失败，导致整条链路都失败的情形 解决方案 降级（接口） Feign 接口走fallback 熔断（整个服务） Hystix @EnableCircuitBreaker 断路器关闭 - 打开 - 半开 你们的微服务是怎么监控的？ skywalking apache顶级项目 问题定位 性能分析 服务关系 服务告警 业务相关 项目中有没有做过限流？怎么做的？ 限流情况\n并发量大 防止用户恶意刷接口 限流实现方式\nTomcat 设置最大连接数 适合单体，微服务不太行 Nginx 漏桶算法 漏桶以固定的速率漏出请求 控制并发连接数 网关 令牌桶算法 filters name: RequestRateLimiter key-resolver redis-rate-limiter.replenishRate redis-rate-limiter-burstCapacity 自定义拦截器 令牌桶和漏桶的区别 解释一下CAP和BASE 分布式系统的三个指标 C 一致性 A 可用性 P 分区容错性 BASE 理论 Basically Available Soft State Eventually Consistent 分布式事务解法家族 2PC/XA TCC Saga 本地消息表 / 事务性 Outbox 你们项目中采用哪种分布式事务的解决方案 Seata TC TM RM 各种模式 XA AT TCC MQ分布式事务 分布式服务的接口幂等性如何设计 需要幂等的场景 用户重复点击 MQ消息重复 应用使用失败或超时重试机制 幂等保护的操作对象 新增的数据 修改的数据 基于 RESTful API的角度对常见类型请求的幂等性进行分析 GET 天然幂等 POST 不是幂等 PUT 直接SET更新为某个固定常量是幂等，SET引用之前的数据，不是幂等 DELETE 幂等 解决接口幂等 token + redis 生成唯一token存入redis，返回给前端 第二次请求携带之前token到redis进行颜值 分布式锁（性能比较低） 你们项目中使用了什么样的分布式任务调度 xxl-job 解决的问题 集群任务重复执行 cron表达式定义灵活 定时任务失败了，重试和统计 任务量大，分片执行 xxl-job的路由策略 ROUND FAILOVER SHARDING_BROADCAST xxl-job的路由策略？ xxl-job任务执行失败怎么解决？ 故障转移 + 失败重试 查看日志分析 -\u0026gt; 邮件告警\n如果有大数据量的任务需要同时执行，怎么解决？ 其他 OpenFeign 服务调用过程 flowchart TD A[\"业务代码调用userClient.getById(1L)\"] --\u003e B[\"JDK动态代理 Proxy.invoke\"] B --\u003e C[\"Feign InvocationHandlerMethodHandler\"] C --\u003e D[\"Contract解析注解@GetMapping / @PathVariable\"] D --\u003e E[\"RequestTemplate构建HTTP请求\"] E --\u003e F[\"参数绑定Path / Query / Body / Header\"] F --\u003e G[\"服务名解析 user-service\"] G --\u003e H[\"服务发现 Nacos / Eureka\"] H --\u003e I[\"LoadBalancer负载均衡\"] I --\u003e J[\"选择实例 IP:PORT\"] J --\u003e K[\"HTTP Client执行器OkHttp / Apache / JDK\"] K --\u003e L[\"发送HTTP请求 GET /user/1\"] L --\u003e M[\"远程服务 user-service\"] M --\u003e N[\"Controller处理请求\"] N --\u003e O[\"返回JSON\"] O --\u003e P[\"Feign Decoder反序列化\"] P --\u003e Q[\"JSON → UserDTO\"] Q --\u003e R[\"返回业务代码\"] ","permalink":"https://lv-blog.pages.dev/posts/programming/for-interview/spring-cloud-interview-questions/","summary":"\u003ch1 id=\"springcloud-问题\"\u003eSpringCloud 问题\u003c/h1\u003e\n\u003ch2 id=\"springcloud-五大组件\"\u003espringcloud 五大组件\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eEureka / nacos\u003c/li\u003e\n\u003cli\u003eRibbon\u003c/li\u003e\n\u003cli\u003eFeign / dubbo\u003c/li\u003e\n\u003cli\u003eHystrix / sentinel\u003c/li\u003e\n\u003cli\u003eZuul/Gateway\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"服务注册和发现是什么意思springcloud-如何实现服务注册和发现\"\u003e服务注册和发现是什么意思，SpringCloud 如何实现服务注册和发现？\u003c/h2\u003e\n\u003ch2 id=\"nacos-和-eureka-的区别\"\u003enacos 和 eureka 的区别？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e相同点\n\u003cul\u003e\n\u003cli\u003e服务提供者 注册 服务信息 到 注册中心\u003c/li\u003e\n\u003cli\u003e服务消费者 定时从 注册中心 拉取\u003c/li\u003e\n\u003cli\u003e服务消费者 对 服务提供者 进行远程调用\u003c/li\u003e\n\u003cli\u003e服务提供者 临时实例 采用心跳检查\n\u003cul\u003e\n\u003cli\u003e什么是临时实例？\n\u003cul\u003e\n\u003cli\u003e默认实例都是临时实例\u003c/li\u003e\n\u003cli\u003e临时实例心跳模式\u003c/li\u003e\n\u003cli\u003e非临时主动探测\u003c/li\u003e\n\u003cli\u003e临时实例没有心跳会被剔除\u003c/li\u003e\n\u003cli\u003e非临时不会被剔除\u003c/li\u003e\n\u003cli\u003eephemeral: false 变成非临时实例\n\u003cul\u003e\n\u003cli\u003e非临时实例，nacos会主动询问服务提供者是否存活\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e不同点\n\u003cul\u003e\n\u003cli\u003e非临时实例\u003c/li\u003e\n\u003cli\u003enacos集群AP，如果存在非临时实例，采用CP\u003c/li\u003e\n\u003cli\u003eeureka采用AP\u003c/li\u003e\n\u003cli\u003enacos还有配置中心\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"你们项目中负载均衡是如何实现的\"\u003e你们项目中负载均衡是如何实现的？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eRibbon\u003c/li\u003e\n\u003cli\u003eRibbon 负载均衡流程\n\u003col\u003e\n\u003cli\u003e微服务发起请求\u003c/li\u003e\n\u003cli\u003eRibbon 从注册中心拉取\u003c/li\u003e\n\u003cli\u003e根据策略进行转发\u003c/li\u003e\n\u003c/ol\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"ribbon-负载均衡策略有哪些\"\u003eRibbon 负载均衡策略有哪些？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eRoundRobinRule\u003c/strong\u003e 简单轮询\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eWeightedResponseTimeRule\u003c/strong\u003e 按照权重来选择服务器，响应时间越长，权重越小\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eRandomRule\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003eBestAvailableRule 忽略短路的服务器，选择并发数较低的服务器\u003c/li\u003e\n\u003cli\u003eRetryRule 重试机制的选择逻辑（基于轮询）\u003c/li\u003e\n\u003cli\u003eAvailabilityFilteringRule 可用性敏感策略。先过滤非健康的，再选择连接数比较小的\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eZoneAvoidanceRule\u003c/strong\u003e 以区域可用的服务器为基础对服务器选择。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"如果想自定义负载均衡策略如何实现\"\u003e如果想自定义负载均衡策略，如何实现？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e自己创建类实现 IRule 接口， 再通过配置类或配置文件配置\u003c/li\u003e\n\u003cli\u003e@Bean IRule 全局生效\u003c/li\u003e\n\u003cli\u003eNFLoadBalancerRuleClassName 局部生效\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"什么是服务雪崩如何解决这个问题\"\u003e什么是服务雪崩？如何解决这个问题？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e服务雪崩\n\u003cul\u003e\n\u003cli\u003e一个服务失败，导致整条链路都失败的情形\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e解决方案\n\u003cul\u003e\n\u003cli\u003e降级（接口）\n\u003cul\u003e\n\u003cli\u003eFeign 接口走fallback\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e熔断（整个服务）\n\u003cul\u003e\n\u003cli\u003eHystix\n\u003cul\u003e\n\u003cli\u003e@EnableCircuitBreaker\u003c/li\u003e\n\u003cli\u003e断路器关闭 - 打开 - 半开\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"你们的微服务是怎么监控的\"\u003e你们的微服务是怎么监控的？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eskywalking\n\u003cul\u003e\n\u003cli\u003eapache顶级项目\u003c/li\u003e\n\u003cli\u003e问题定位\u003c/li\u003e\n\u003cli\u003e性能分析\u003c/li\u003e\n\u003cli\u003e服务关系\u003c/li\u003e\n\u003cli\u003e服务告警\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch1 id=\"业务相关\"\u003e业务相关\u003c/h1\u003e\n\u003ch2 id=\"项目中有没有做过限流怎么做的\"\u003e项目中有没有做过限流？怎么做的？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e限流情况\u003c/p\u003e","title":"Spring Cloud 面试题整理"},{"content":"集合 为什么数组索引要从 0 开始？从 1 开始不行吗？ 回答大纲\n数组在内存中是连续存储的，CPU 通过寻址公式访问元素 从 0 开始：a[i]_address = base_address + i * typeSize 从 1 开始：base_address + (i-1) * typeSize，多一次减法 正式回答\n数组在内存中是连续存储的，CPU 通过寻址公式来访问指定下标的元素：a[i] 的地址 = 数组首地址 + i * 元素大小。当数组下标从 0 开始时，直接套用上述公式即可计算偏移量；如果下标从 1 开始，则需要先执行 i - 1 再做乘法，多了一次减法运算。虽然单次访问的差异可以忽略不计，但数组通常会出现在循环、遍历等高频访问的场景中，这笔额外开销会被无限放大。这也是 C 语言沿用下来的历史约定，Java 作为一门追求高效的系统级语言，继承了这一设计。\n时间复杂度如何计算的？ 回答大纲\n口诀：常 对 幂 指 阶 常见复杂度 常数复杂度 O(1)：不随 n 变化 对数复杂度 O(logN)：二分查找、树操作 正式回答\n时间复杂度用于衡量算法执行时间随数据规模 n 增长的变化趋势，是大 O 表示法下对最坏情况的抽象描述。常见的大小关系可用口诀\u0026quot;常对幂指阶\u0026quot;来记忆，即 O(1) \u0026lt; O(logN) \u0026lt; O(N) \u0026lt; O(N*logN) \u0026lt; O(N²) \u0026lt; O(2ⁿ) \u0026lt; O(n!)，越靠后算法越慢。\n常数复杂度 O(1)：只要代码执行次数不随 n 的增大而增大，都是常数复杂度。比如简单的赋值、算术运算、HashMap 在不发生哈希冲突时的查找。 对数复杂度 O(logN)：典型场景包括二分查找、堆的插入、平衡二叉搜索树的查找等。每次操作都把问题规模缩减为原来的一部分。需要注意的是，对数的底在大 O 表示法下并无差异，O(log₂N) 和 O(log₁₀N) 等价。 分析一段代码的时间复杂度时，要先剥离常数项和低阶项，只保留最高阶项，并关注最坏情况下的复杂度。\nArrayList 底层的实现原理是什么？ 回答大纲\n动态数组 Object[] elementData 默认容量 10，懒加载 扩容为原容量的 1.5 倍 通过 Arrays.copyOf 实现扩容 正式回答\nArrayList 是基于动态数组实现的 List，其核心是一个 Object[] elementData 数组。它具有以下特点：\n默认容量：使用无参构造创建时，并不会立即分配容量为 10 的数组，而是采用懒加载策略，在第一次执行 add 时才把数组容量初始化为 10（JDK 8 行为）。 扩容机制：当元素个数超过当前数组容量时触发扩容，新容量约为旧容量的 1.5 倍，计算方式为 oldCapacity + (oldCapacity \u0026gt;\u0026gt; 1)。扩容过程通过 Arrays.copyOf 将旧数组元素复制到新数组，因此扩容操作本身是 O(N) 的时间复杂度，应尽量预估容量避免频繁扩容。 随机访问高效：底层是连续数组，支持 O(1) 时间复杂度的随机访问。 插入和删除低效：在中间位置插入或删除元素时，需要把该位置之后的所有元素整体后移或前移，最坏时间复杂度为 O(N)。 线程不安全：所有操作均未加同步控制，多线程环境下应使用 Collections.synchronizedList 或 CopyOnWriteArrayList。 ArrayList list = new ArrayList(10) 中的数组扩容几次？ 回答大纲\n该构造器直接初始化长度为 10 的数组 没有调用 add，因此扩容 0 次 只有在添加第 11 个元素时才会触发第一次扩容（10 → 15） 正式回答\nnew ArrayList(10) 构造器被调用时，会立即创建一个长度为 10 的 Object[] elementData 数组，这与无参构造的懒加载行为不同。题目中只调用了构造器，并未执行任何 add 操作，因此扩容次数为 0 次。\n这道题考察的是对 ArrayList 构造器参数语义和扩容触发时机的理解：\nnew ArrayList()：使用空数组，第一次 add 时才创建容量为 10 的数组。 new ArrayList(10)：直接创建容量为 10 的数组，再次 add 时超过 10 才会扩容至 15。 如果题目改为\u0026quot;调用了 11 次 add\u0026quot;，则扩容次数为 1 次。 如何实现数组和 List 之间的转换？ 回答大纲\n数组转 List：Arrays.asList(array) List 转数组：list.toArray() 或 list.toArray(T[]) 转换后的影响 asList 返回的是 Arrays 内部类视图，底层直接引用原数组 修改原数组会影响 List；但无法 add / remove toArray 返回的是新数组，修改不影响 List 正式回答\n数组转 List：Arrays.asList(T... a)，返回一个固定大小的 List。注意以下几点：\n该 List 底层直接引用原数组，不是新数组。因此修改原数组的元素，List 中对应位置的元素也会随之变化。 但是它并未实现 add 和 remove 方法，调用会抛出 UnsupportedOperationException。 如果需要一个可变的 List，应该使用 new ArrayList\u0026lt;\u0026gt;(Arrays.asList(array)) 再封装一层。 List 转数组：list.toArray() 或 list.toArray(T[] a)。\n返回的是新数组，修改新数组不会影响原 List。 建议使用 list.toArray(new T[list.size()])，预先分配与 List 等长的数组，性能略优于 new T[0]。 不传参数调用的 toArray() 返回的是 Object[]，强转为具体类型会有 ClassCastException 风险。 ArrayList 和 LinkedList 的区别是什么？ 回答大纲\n底层数据结构：动态数组 vs 双向链表 操作效率：随机访问 O(1) vs O(N)；插入删除 O(N) vs O(1) 内存占用：ArrayList 更紧凑，LinkedList 需要额外指针 线程安全性：均为线程不安全 正式回答\n对比维度 ArrayList LinkedList 底层数据结构 动态数组 Object[] 双向链表 Node\u0026lt;E\u0026gt; 随机访问 O(1)，索引直达 O(N)，需要遍历 头插 / 尾插 O(N)（尾插均摊 O(1)） O(1) 中间插入 / 删除 O(N)，需要移动元素 O(1)（前提是已定位节点） 内存占用 连续内存，浪费少 每个节点额外存前后指针 线程安全 不安全 不安全 ArrayList 基于动态数组实现，支持 O(1) 的随机访问，但插入删除需要移动元素。LinkedList 基于双向链表实现，定位后插入删除很快，但随机访问需要遍历。在现代 JVM 中，由于 CPU 缓存预读机制，ArrayList 的连续内存布局在遍历时缓存命中率更高，实际性能常常优于 LinkedList。因此实际开发中应优先使用 ArrayList，只有在频繁头插或者已经持有迭代器进行中间删除时才考虑 LinkedList。\n二叉搜索树数据结构 回答大纲\n性质：左子树 \u0026lt; 根 \u0026lt; 右子树 时间复杂度 查找 O(logN) 插入 O(logN) 删除 O(logN) 极端情况退化为 O(N) 正式回答\n二叉搜索树（Binary Search Tree, BST）的核心性质是：对于每个节点，其左子树上所有节点的值都小于该节点的值，右子树上所有节点的值都大于该节点的值。基于这种有序结构，它有以下时间复杂度特性：\n查找 O(logN)：从根节点开始比较，每次比较都能排除掉一半的子树。若节点在第 n 层，则最多需要比较 n-1 次，时间复杂度为 O(logN)。 插入 O(logN)：遵循查找路径定位到合适位置后插入，整体过程与查找类似。 删除 O(logN)：先查找到目标节点，然后根据子节点情况分类处理：无子节点直接删除；单子节点让孩子接替；双子节点则需找到右子树的最小节点（或左子树的最大节点）填补。 极端情况：当插入有序序列（如 1、2、3、4、5）时，BST 会退化为链表，此时所有操作的时间复杂度退化为 O(N)。 为解决退化问题，引入了自平衡的二叉搜索树，如 AVL 树和红黑树，它们通过旋转等手段维持树的平衡。\n红黑树数据结构 回答大纲\n自平衡的二叉搜索树 五大性质：节点红黑 / 根黑 / 叶黑 / 红不连续 / 黑高相同 时间复杂度 O(logN) 正式回答\n红黑树（Red-Black Tree）是一种自平衡的二叉搜索树，它在每个节点上增加了一个颜色标志位（红色或黑色），通过对从根到叶子路径的颜色施加约束来保证树的\u0026quot;大致平衡\u0026quot;。其五大性质如下：\n节点颜色：每个节点要么是红色，要么是黑色。 根节点颜色：根节点是黑色。 叶子节点：所有叶子节点（NIL 空节点）都是黑色。 红色节点不连续：红色节点的子节点必须是黑色，即不能出现两个连续的红色节点。 黑高平衡：从任一节点到其所有叶子节点的路径上，包含的黑色节点数目相同（也称黑高相同）。 这五条性质保证了最长路径不超过最短路径的 2 倍，因此树始终维持平衡，查找、插入、删除的最坏时间复杂度均为 O(logN)。相比 AVL 树，红黑树牺牲了部分严格平衡性以换取更少的旋转次数，插入删除性能更稳定，因此被广泛用于 JDK 的 TreeMap、HashMap（链表长度 ≥ 8 时树化）、ConcurrentHashMap 等核心数据结构中。\n散列表（Hash 表） 回答大纲\n由数组演化而来 哈希函数将 key 映射到数组下标 哈希冲突与处理：链表法 / 开放地址法 时间复杂度 O(1) 正式回答\n散列表（Hash Table）也称哈希表，是基于数组结构演化而来的高效查找结构。它通过哈希函数将任意大小的 key 映射到固定范围的数组下标，从而实现近似 O(1) 的查找、插入和删除。\n但是，由于 key 的取值空间往往远大于数组容量，哈希冲突不可避免。当多个 key 被映射到同一个下标时，需要采用合适的策略处理。常见的两类冲突处理方式是：\n链表法（拉链法）：在每个桶位维护一个链表，所有哈希冲突的元素挂在同一个链表中。Java 的 HashMap 即采用此方案，链表长度过大时会进一步转换为红黑树。 开放地址法：当发生冲突时，按照某种探测序列（如线性探测、二次探测）在数组其他空位上存储元素。ThreadLocalMap 即采用此方案。 理想情况下，散列表各项操作时间复杂度为 O(1)，极端情况下会退化为 O(N)。\n说一下 HashMap 的实现原理？ 回答大纲\nJDK 1.7：数组 + 链表 JDK 1.8：数组 + 链表 + 红黑树 哈希算法定位桶位 链表法解决哈希冲突 正式回答\nHashMap 是基于哈希表实现的 Map 接口的核心实现类，用于存储键值对。\n底层数据结构： JDK 1.7：采用 数组 + 链表，所有哈希冲突元素依次挂载在同一个桶位的链表上。 JDK 1.8：在数组 + 链表的基础上引入红黑树。当链表长度 ≥ 8 且当前数组长度 ≥ 64 时，链表会转换为红黑树，将该桶位上的查询时间复杂度从 O(N) 提升到 O(logN)；当红黑树节点数 ≤ 6 时退化为链表。 哈希计算：通过 key.hashCode() 得到哈希值，再进行二次扰动 (h = key.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16)，最后通过 (n - 1) \u0026amp; hash 计算数组下标。 冲突处理：使用链表法，相同下标的元素挂在同一链表中，链表过长时升级为红黑树。 重要属性：默认初始容量 16，默认加载因子 0.75，扩容时容量翻倍。 HashMap 允许 null 键和 null 值，但只能有一个 null 键。它不是线程安全的，多线程环境下应使用 ConcurrentHashMap。\nJDK 1.7 和 JDK 1.8 HashMap 的区别？ 回答大纲\n数据结构：1.7 数组+链表；1.8 数组+链表+红黑树 插入方式：1.7 头插法；1.8 尾插法 哈希扰动：1.7 4 次位运算 + 4 次异或；1.8 1 次位运算 + 1 次异或 扩容时链表处理：1.7 全部重哈希；1.8 高低位链表拆分 死循环：1.7 多线程下可能形成环形链表；1.8 已修复 正式回答\n对比项 JDK 1.7 JDK 1.8 数据结构 数组 + 链表 数组 + 链表 + 红黑树 链表转红黑树 无 链表长度 ≥ 8 且数组长度 ≥ 64 插入方式 头插法 尾插法 哈希扰动 4 次位运算 + 5 次异或 1 次位运算 + 1 次异或 扩容链表处理 全部重新哈希 高低位链表拆分 多线程死循环 可能形成环形链表 已修复 详细说明：\n数据结构：JDK 1.8 引入红黑树，避免哈希冲突严重时链表过长导致的查询性能退化，是 HashMap 性能改进的核心。 插入方式：JDK 1.7 使用头插法，多线程扩容时会形成环形链表导致死循环；JDK 1.8 改用尾插法，从根本上避免了链表的反转。 哈希扰动：JDK 1.8 简化为 h ^ (h \u0026gt;\u0026gt;\u0026gt; 16)，仅 1 次右移和 1 次异或，计算更快，且因红黑树的引入，对分布性的依赖也降低了。 扩容策略：JDK 1.8 通过 e.hash \u0026amp; oldCap 判断元素在新数组中的位置是原位置还是原位置 + 旧容量，从而拆分为低位链表和高位链表，避免重新计算 hash。 HashMap 的 put 方法的具体流程？ 回答大纲\n计算哈希、定位桶位 初始化检查（空表则扩容） 桶位判空（直接放入 / 进入冲突流程） 处理冲突（覆盖 / 红黑树 / 链表） 值覆盖：发现旧 Key 则用新值覆盖 扩容检查：超过阈值则触发 resize() 正式回答\nHashMap 的 put 方法核心流程可以概括为以下几步：\n计算哈希：对 Key 的 hashCode() 进行二次扰动计算，得到最终哈希值，进而定位桶位（数组下标）。 初始化检查：如果内部数组为空，先调用 resize() 进行初始化或扩容。 桶位判空： 如果定位到的桶位为空，直接创建新节点放入。 如果不为空（发生哈希冲突），则进入冲突处理流程。 处理冲突： Key 相同：若首节点的 Key 与待插入 Key 完全相同（equals 为 true），记录该节点以便后续覆盖。 红黑树：若首节点是 TreeNode（已树化），则调用红黑树的插入逻辑。 链表：若为普通链表，遍历至尾部插入。遍历时若发现 Key 相同则跳出；插入后若链表长度 ≥ 8 且数组长度 ≥ 64，则将链表转为红黑树。 值覆盖：若发现存在的旧 Key，用新值覆盖旧值，并返回旧值。 扩容检查：插入新节点后，size 加 1。当 size 超过扩容阈值（capacity * loadFactor）时，触发 resize() 自动扩容。 flowchart TD A[\"开始: put(key, value)\"] --\u003e B[\"计算hash: key.hashCode() ^ (hash \u003e\u003e\u003e 16)\"] B --\u003e C{\"table是否为空 或 长度=0?\"} C --\u003e|是| D[\"resize() 初始化/扩容\"] C --\u003e|否| E[\"通过 (n-1)\u0026hash 计算索引 i\"] D --\u003e E E --\u003e F{\"table[i] 是否为空?\"} F --\u003e|是| G[\"创建新节点插入 table[i]\"] F --\u003e|否| H{\"table[i] 是否为树节点?\"} H --\u003e|是| I[\"向红黑树中插入节点\"] H --\u003e|否| J[\"遍历链表\"] J --\u003e K[\"遍历链表每个节点\"] K --\u003e L{\"节点hash和key是否和目标相等?\"} L --\u003e|是| M[\"找到已有key 记录旧值\"] L --\u003e|否| N{\"是否到达链表末尾?\"} N --\u003e|否| K N --\u003e|是| O[\"在链表尾部插入新节点\"] O --\u003e P{\"链表长度 \u003e= 8?\"} P --\u003e|是| Q{\"数组长度 \u003e= 64?\"} P --\u003e|否| R{\"是否找到已有key?\"} Q --\u003e|是| Q2[\"将链表转为红黑树\"] Q --\u003e|否| D M --\u003e S{\"是否允许覆盖旧值?\"} S --\u003e|是| T[\"用新值替换旧值 返回旧值\"] S --\u003e|否| U[\"返回旧值 不修改\"] I --\u003e R G --\u003e V[\"modCount++\"] Q2 --\u003e V R --\u003e|否| V R --\u003e|是| S V --\u003e W[\"size++\"] W --\u003e X{\"size \u003e threshold?\"} X --\u003e|是| Y[\"resize() 扩容\"] X --\u003e|否| Z[\"返回 null\"] Y --\u003e Z T --\u003e AA[\"结束\"] U --\u003e AA Z --\u003e AA HashMap 的扩容机制？ 回答大纲\n计算新容量与阈值（正常扩容翻倍 / 初始化特殊处理） 创建新数组 数据迁移（高低位链表拆分，单节点 / 红黑树 / 链表三种情况） 正式回答\nHashMap 的扩容（resize 方法）核心流程可简述为以下三步：\n计算新容量与阈值： 正常扩容：若数组已初始化，新容量和新阈值通常直接翻倍（左移 1 位），即 newCap = oldCap \u0026lt;\u0026lt; 1、newThr = oldThr \u0026lt;\u0026lt; 1。若超过最大容量则不再扩容，阈值设为 Integer.MAX_VALUE。 初始化：若数组为空（oldTab == null），根据是否带参构造将容量初始化为指定值或默认值 16，阈值设为 容量 * 加载因子。 创建新数组：根据计算出的新容量，实例化一个新的 Node 数组。 数据迁移（高低位拆分）：遍历旧数组的每个桶位： 单节点：重新计算下标 (e.hash \u0026amp; (newCap - 1)) 直接放入新数组。 红黑树：调用 split 方法拆分并迁移，必要时退化为链表。 普通链表：通过 hash \u0026amp; oldCap 是否为 0，把链表拆分为低位链表（留在原位置）和高位链表（新位置 = 原位置 + 旧容量），整体搬运到新数组。这种方式避免了 JDK 7 中重新计算 hash 的开销，也规避了头插法导致的死循环问题。 graph TD A[开始扩容 resize] --\u003e B{OldCap \u003e 0?} B -- 是 (数组已初始化) --\u003e C{OldCap \u003e= MAXIMUM_CAPACITY?} C -- 是 --\u003e D[阈值设为 Integer.MAX_VALUE, 返回旧数组] C -- 否 --\u003e E[新容量 = 旧容量 \u003c\u003c 1 新阈值 = 旧阈值 \u003c\u003c 1] B -- 否 (未初始化) --\u003e F{OldThr \u003e 0?} F -- 是 (带参构造) --\u003e G[新容量 = 旧阈值] F -- 否 (无参构造) --\u003e H[新容量 = 16 新阈值 = 16 * 0.75] E --\u003e I[创建新数组 newtable] G --\u003e I H --\u003e I I --\u003e J[遍历旧数组的每个桶位] J --\u003e K{桶位是否为空?} K -- 是 --\u003e L[跳过] K -- 否 --\u003e M{是否为单节点?} M -- 是 --\u003e N[直接计算新位置: e.hash \u0026 newCap - 1 放入新数组] M -- 否 --\u003e O{是否为树节点 TreeNode?} O -- 是 --\u003e P[调用 split 方法拆分红黑树] O -- 否 --\u003e Q[链表拆分: 高低位链表 根据 e.hash \u0026 oldCap == 0] Q --\u003e R[低位链表: 位置保持不变 高位链表: 新位置 = 原位置 + oldCap] L --\u003e S{是否遍历完所有桶?} N --\u003e S P --\u003e S R --\u003e S S -- 否 --\u003e J S -- 是 --\u003e T[返回新数组, 扩容完成] HashMap 的寻址算法？ 回答大纲\n扰动函数：hash = key.hashCode() ^ (key.hashCode() \u0026gt;\u0026gt;\u0026gt; 16) 索引计算：(n - 1) \u0026amp; hash，等价于 hash % n n 必须是 2 的次幂以保证取模效果 正式回答\nHashMap 的寻址算法分为两步：\n哈希扰动（JDK 1.8）： static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h \u0026gt;\u0026gt;\u0026gt; 16); } 将 hashCode 的高 16 位与低 16 位进行异或，混合高低位的特征，避免数组容量较小时（仅低位有效）发生严重的哈希冲突。 索引计算： int index = (n - 1) \u0026amp; hash; // 等价于 hash % n 通过位与运算代替取模运算，前提是 n 是 2 的次幂，n - 1 的二进制是全 1，\u0026amp; hash 即等同于 hash % n。 为什么使用位运算：位运算比取模运算快得多；且当 n 为 2 的次幂时，二者结果完全等价。 为什么 HashMap 的数组长度一定是 2 的次幂？ 回答大纲\n位运算代替取模运算 扩容时高效（仅需左移一位） 数据分布均匀 非 2 次幂容量会被 tableSizeFor 自动调整 正式回答\nHashMap 在构造和扩容时，会通过 tableSizeFor 方法保证数组容量始终是 2 的次幂。这样设计主要有三个原因：\n位运算代替取模：当数组长度为 2 的次幂时，(n - 1) \u0026amp; hash 与 hash % n 等价，而位运算效率远高于取模运算，在高频调用下能显著提升性能。 扩容高效：扩容时新容量直接 oldCap \u0026lt;\u0026lt; 1（左移一位，即翻倍），(n - 1) \u0026amp; hash 中的 n - 1 也只需多一位全 1，避免了重新计算 hash，并支持高低位链表拆分的高效迁移策略。 数据分布均匀：哈希扰动 + 容量为 2 的次幂，可以让元素均匀分布，减少哈希冲突。 如果用户传入的初始容量不是 2 的次幂（如 10），HashMap 会通过 tableSizeFor 方法自动调整为大于等于该值的最小 2 的次幂。例如传入 10 会被调整为 16，传入 17 会被调整为 32。\nHashMap 在 JDK 1.7 下的多线程死循环问题 回答大纲\n头插法 + 并发扩容导致环形链表 触发条件：多线程同时触发 put + resize 现象：get 时死循环，CPU 飙升至 100% 解决：JDK 1.8 改用尾插法 + 高低位拆分；或使用 ConcurrentHashMap 正式回答\nJDK 1.7 中 HashMap 在多线程并发扩容时可能产生死循环问题，核心原因是头插法与并发 resize 共同导致的。\n问题复现过程：\n假设线程 A 和线程 B 同时对 HashMap 进行 put 操作，并先后触发扩容。 线程 A 已经遍历到某个桶位的链表（例如 key=3 → key=7 → null），正准备按头插法迁移。 线程 B 同时开始执行相同的迁移流程，由于头插法会反转链表顺序。 当两个线程交替执行 next = e.next; e.next = newTable[i]; newTable[i] = e; e = next; 这几行代码时，可能形成环形链表：key=3.next = key=7, key=7.next = key=3。 此后调用 get 查询落在该桶位的元素时，就会陷入无限循环，CPU 使用率飙升至 100%。 修复方案：\nJDK 1.8 改用了尾插法配合高低位链表拆分机制，从根本上避免了链表的反转过程，从而消除了环形链表的形成条件。但是尾插法并不解决数据丢失等更复杂的并发问题，因此对于真正的并发场景，应使用 ConcurrentHashMap 替代 HashMap。\nJDK 1.8 是如何修复该问题的？ 回答大纲\n尾插法代替头插法 扩容时使用高低位链表拆分，避免链表反转 仍存在其他并发问题（数据丢失等），需用 ConcurrentHashMap 正式回答\nJDK 1.8 主要从以下几个方面对 JDK 1.7 的多线程死循环问题进行了修复：\n改用尾插法：新元素插入到链表的尾部，而不是头部。在扩容迁移时，由于始终是尾插法，链表的节点顺序不会发生反转，从根本上消除了环形链表的形成条件。 高低位链表拆分：在 resize 过程中，通过 e.hash \u0026amp; oldCap 把链表拆分为低位链表（位置不变）和高位链表（位置 = 原位置 + 旧容量），整体搬运到新数组。这种方式不依赖于节点指针的反转，而是节点顺序的直接搬运，进一步降低了并发风险。 HashMap 仍未解决所有并发问题：例如并发 put 时仍可能出现数据丢失、size 不一致等，因此仍需在并发场景下使用 ConcurrentHashMap。ConcurrentHashMap 在 JDK 1.7 中采用分段锁，在 JDK 1.8 中改为 CAS + synchronized 实现，锁粒度更细，并发性能更优。 多线程 进程和线程的区别 回答大纲\n进程 是资源分配的基本单位，管理指令、内存、IO 不同进程拥有独立的内存空间 进程下的线程共享内存空间 线程 是 CPU 调度的基本单位，是指令流 线程更轻量，上下文切换成本低 同一进程内的线程共享进程的内存和资源 正式回答\n进程是操作系统进行资源分配的基本单位，每启动一个进程，操作系统会为其分配独立的内存地址空间、IO 通道等资源。进程负责加载指令、管理内存、管理 IO；不同进程之间拥有各自独立的内存空间，进程内的所有线程共享这块内存空间。\n线程是 CPU 调度执行的基本单位，是一条有序的指令流。线程自身只负责把指令一条条交给 CPU 执行，并不拥有独立的系统资源（如内存、文件句柄）。因此线程更轻量，线程切换（上下文切换）的成本远低于进程切换。\n简单说，进程是\u0026quot;资源容器\u0026quot;，线程是\u0026quot;执行单元\u0026quot;，一个进程至少包含一个线程（主线程）。这也是 Java 中 Thread.start() 不一定立刻执行，而是由 JVM 线程调度器决定的原因。\n并行和并发的区别 回答大纲\n并发 多个线程轮流使用 CPU（单核也可发生） 微观串行，宏观像并行 并行 必须多核 CPU，每个核心真正同时执行任务 微观也是同时进行 正式回答\n**并发（Concurrency）**指多个任务在同一时间段内交替执行，但在某一时刻通常只有一个任务真正占用 CPU。它强调的是\u0026quot;处理能力\u0026quot;，单核 CPU 上也能发生并发，因为 CPU 通过时间片轮转调度让多个线程轮流使用处理器，从微观上看是串行的，但从用户视角看像是同时进行的。\n并行（Parallelism）指多个任务在同一时刻真正同时执行，它要求系统具备多个 CPU 核心，每个核心各自执行不同的任务，从微观上看也是同时的。\n并发是逻辑上的同时，并行是物理上的同时。前者关注结构（如何编写可处理多任务的程序），后者关注执行（如何让任务真正同时运行）。现代高并发系统往往会结合两者：先用并发设计程序结构，再借助多核 CPU 实现并行执行，最大化利用硬件资源。\n创建线程的方式有哪些？ 回答大纲\n继承 Thread 类 实现 Runnable 接口 实现 Callable 接口（有返回值、可抛异常） 通过线程池（项目中使用） 正式回答\nJava 创建线程主要有以下四种方式：\n继承 Thread 类：定义类继承 Thread，重写 run 方法，调用 start() 启动。简单直观，但 Java 是单继承的，占用了继承名额，扩展性较差。 实现 Runnable 接口：定义类实现 Runnable 接口，重写 run 方法，将其实例作为参数传给 new Thread(runnable)。推荐方式，因为不影响类继承其他父类，且天然支持 Lambda 表达式。 实现 Callable 接口：与 Runnable 类似，但 call 方法可以有返回值，并能抛出受检异常，通常与 Future 配合使用拿到异步结果：FutureTask\u0026lt;T\u0026gt; task = new FutureTask\u0026lt;\u0026gt;(callable); new Thread(task).start();。 通过线程池（ExecutorService）：实际项目中强烈推荐。将任务作为 Runnable/Callable 提交给线程池，由线程池统一管理线程的生命周期。能避免频繁创建销毁线程的开销，支持任务队列、线程数控制、拒绝策略等高级特性。常见工厂类有 Executors.newFixedThreadPool 等，但不推荐直接使用 Executors，而是手动创建 ThreadPoolExecutor 以明确参数语义。 runnable 和 callable 的区别 回答大纲\n返回值 Runnable.run() 无返回值 Callable.call() 有返回值，可通过 Future 获取 抛异常 run 方法不能抛出受检异常 call 方法可以抛出受检异常 实现方法名：run() vs call() 正式回答\nRunnable 和 Callable 都是描述\u0026quot;一段可执行任务\u0026quot;的接口，二者主要有两个区别：\n返回值：Runnable 的 run() 方法没有返回值；而 Callable 的 call() 方法有返回值（类型由泛型参数决定），可通过 Future.get() 同步获取结果。 异常：run() 方法被设计为不能抛出任何受检异常，只能通过 try-catch 自行处理；而 call() 方法可以抛出受检异常，由调用方通过 Future.get() 时统一包装为 ExecutionException。 此外，Callable 接口定义的方法名是 call，而 Runnable 是 run。两者均能被 ExecutorService 接收并执行，但 Callable 通常配合 FutureTask 使用，是构建异步任务和并行计算（如 parallelStream 底层、CompletableFuture）的基础。\n在启动线程的时候，可以使用 run 方法吗？run 和 start 的区别？ 回答大纲\n不能用 run 启动线程，应使用 start run 是普通方法调用，在当前线程同步执行 start 会启动新线程，由 JVM 调用新线程的 run 方法 start 只能调用一次（线程状态不能回到 NEW） 正式回答\n启动线程必须调用 start() 方法，不能直接调用 run() 方法。\nrun() 方法：只是 Thread 类的普通成员方法，直接调用时，是在当前线程同步执行其中的代码，并不会启动新线程，等价于普通方法调用。 start() 方法：会触发 JVM 创建一个新的操作系统线程，并在新线程中由 JVM 回调该线程的 run() 方法，这才是真正意义上的多线程并发。 线程状态：start() 只能被调用一次，因为线程启动后状态从 NEW 变为 RUNNABLE，再次调用会抛出 IllegalThreadStateException。 简而言之，run 定义任务内容，start 启动新线程。混用两者是初学者常见错误，会导致程序\u0026quot;看起来在多线程，但实际是顺序执行\u0026quot;的隐性 bug。\n线程包括哪些状态，状态之间是如何变化的？ 回答大纲\n六种状态：NEW / RUNNABLE / BLOCKED / WAITING / TIMED_WAITING / TERMINATED 状态转换：start() → RUNNABLE → 各种阻塞状态 → RUNNABLE → TERMINATED 正式回答\nJava 中线程有 6 种状态，定义在 Thread.State 枚举中：\nNEW（新建）：线程对象已创建，但尚未调用 start()。 RUNNABLE（可运行）：调用 start() 后，线程处于就绪或正在运行的状态。注意：Java 的 RUNNABLE 同时包含了\u0026quot;就绪\u0026quot;和\u0026quot;运行中\u0026quot;两种含义。 BLOCKED（阻塞）：线程等待获取 monitor 锁（如 synchronized 块竞争失败），被放入 EntryList。 WAITING（无限等待）：调用 wait()、join()、LockSupport.park() 后进入，需要其他线程 notify/notifyAll 或 unpark 才能唤醒。 TIMED_WAITING（限时等待）：调用 sleep(long)、wait(long)、join(long)、LockSupport.parkNanos() 等带超时参数的方法进入，超时后会自动唤醒。 TERMINATED（终止）：run() 方法执行完毕或抛出未捕获异常后进入，不可再启动。 状态流转图（核心链路）：\nNEW --start()--\u0026gt; RUNNABLE --获得锁--\u0026gt; RUNNABLE ↓ wait() WAITING --notify()/超时--\u0026gt; BLOCKED(若需重获锁) -\u0026gt; RUNNABLE ↓ sleep(50) TIMED_WAITING --超时--\u0026gt; RUNNABLE ↓ 竞争 monitor 失败 BLOCKED --获得锁--\u0026gt; RUNNABLE ↓ run() 结束 TERMINATED 新建 T1，T2，T3 三个线程如何保证执行顺序？ 回答大纲\n使用 Thread.join() 让后启动的线程等待先启动的执行完 也可以使用 CountDownLatch、CyclicBarrier 最简单：依次 t1.start(); t1.join(); t2.start(); t2.join(); t3.start(); 正式回答\n若要保证三个线程严格按照 T1 → T2 → T3 的顺序执行，可以有以下几种方案：\nThread.join()（最常用）： t1.start(); t1.join(); t2.start(); t2.join(); t3.start(); t3.join(); join() 的作用是让当前线程等待被调用 join 的线程执行完毕。因此主线程会先等 t1 跑完，再启动 t2 并等待，再启动 t3 并等待，从而保证顺序。 CountDownLatch：初始化 CountDownLatch latch = new CountDownLatch(1)，每个线程 await，启动顺序由\u0026quot;先 latch.countDown() 再启动下一个\u0026quot;控制。 ExecutorService + Future：提交任务返回 Future，在主线程依次调用 future.get()，实现串行效果。 单线程线程池：Executors.newSingleThreadExecutor()，任务会按提交顺序串行执行。 实际业务中若只是需要\u0026quot;按顺序\u0026quot;，最推荐 join()，简单直观；若涉及并发协调则可用 CountDownLatch 或 CompletableFuture。\nnotify() 和 notifyAll() 有什么区别？ 回答大纲\nnotify()：唤醒等待同一锁的一个线程（具体由 JVM 决定） notifyAll()：唤醒等待同一锁的全部线程 推荐优先使用 notifyAll()，避免信号丢失 二者均必须在 synchronized 块内调用，且只能唤醒调用同一锁对象的线程 正式回答\nnotify() 和 notifyAll() 都是 Object 类的方法，用于在 synchronized 块中唤醒因调用 wait() 而进入 WAITING 状态的线程。\nnotify()：唤醒一个正在等待该对象 monitor 的线程，具体唤醒哪个由 JVM 实现决定（通常是等待时间最久的那个，但并不保证）。被唤醒的线程需要重新竞争 monitor 锁才能继续执行。 notifyAll()：唤醒所有正在等待该对象 monitor 的线程。它们将全部进入阻塞队列，重新竞争 monitor 锁，只有抢到锁的线程能继续执行。 核心差异：notify() 的风险在于\u0026quot;信号丢失\u0026quot;。如果唤醒的线程并不满足继续执行的条件，而其他线程的等待条件又不会被唤醒，就会出现所有线程都在等待的\u0026quot;假死\u0026quot;状态。因此实际开发中推荐优先使用 notifyAll()，以避免此类问题。notify() 仅在能精确控制等待条件（例如只有一种类型的等待者）时才使用。\njava 中 wait 和 sleep 方法的不同 回答大纲\n共同点 都能让线程进入阻塞状态（等待/睡眠） 都可以被 interrupt 打断 不同点 方法归属：wait 属于 Object；sleep 属于 Thread 锁特性：wait 会释放锁；sleep 不释放锁 唤醒方式：wait 需要 notify；sleep 超时自动醒 调用位置：wait 必须在 synchronized 中；sleep 任意 正式回答\nwait() 和 Thread.sleep() 都能暂停线程的执行，但有重要差异：\n方法归属：wait() 是 Object 的实例方法，sleep() 是 Thread 的静态方法。 是否释放锁：wait() 在等待时会释放当前持有的 monitor 锁，让其他线程能进入临界区；sleep() 仅仅让线程休眠，不会释放任何锁，可能导致其他线程长时间阻塞。 唤醒方式：wait() 必须由其他线程在同一对象上调用 notify()/notifyAll() 才能唤醒（无参 wait() 无超时）；sleep(long) 超时后会自动恢复到 RUNNABLE。 使用场景：wait()/notify() 用于线程间协作（如生产者-消费者）；sleep() 用于简单的延时控制。 调用位置：wait() 必须在 synchronized 块内调用（因为要操作 monitor 队列），否则抛 IllegalMonitorStateException；sleep() 可以在任何地方调用。 所属类与异常：wait() 在 Object 中声明，会抛 InterruptedException；sleep() 在 Thread 中，也会抛 InterruptedException。 如何停止一个正在运行的线程 回答大纲\n设置退出标志位（推荐） 已过时的 stop() / destroy() interrupt() 机制 阻塞线程：抛出 InterruptedException 正常线程：通过 Thread.interrupted() 检查并自行退出 正式回答\nJava 中停止线程主要有三种方式：\n退出标志位（推荐）：定义一个 volatile boolean flag = true;，线程内每次循环检查 flag，外部修改 flag = false 后线程自然退出。这种方式安全、协作式，符合\u0026quot;线程不应被强制中断\u0026quot;的设计哲学。 Thread.stop()：已过时。它会直接抛出 ThreadDeath 异常强制终止线程，可能导致共享数据不一致、锁未释放等问题，因此不要再用。 interrupt() 机制：通过 t.interrupt() 给目标线程打上中断标记，分两种情况处理： 阻塞线程（处于 wait()/sleep()/join() 等）：会抛出 InterruptedException，并在抛出后清除中断标记，需要在 catch 中妥善处理（例如 Thread.currentThread().interrupt() 重新设置标记）。 正常运行的线程：不会立即响应，需要在代码中主动通过 Thread.interrupted() 或 Thread.currentThread().isInterrupted() 检查标记并自行退出。 最佳实践是标志位 + interrupt 组合：调用方先设置标志位再调用 interrupt()，线程内部既检查标志位也捕获 InterruptedException，确保退出路径清晰且响应及时。\nsynchronized 关键字的底层原理 回答大纲\n基于对象头中的 Mark Word 和 Monitor 监视器 Monitor 结构 Owner：持有锁的线程 EntryList：等待获取锁的线程 WaitSet：调用 wait() 后等待的线程 锁升级过程 无锁 → 偏向锁 → 轻量级锁 → 重量级锁 正式回答\nsynchronized 是 Java 的关键字，用于实现方法/代码块的互斥访问，底层依赖 JVM 的对象头（Object Header）和 Monitor 监视器机制。\n对象头 Mark Word：每个 Java 对象在内存中都有一个对象头，其中 Mark Word 用于存储对象的哈希码、GC 分代年龄、锁状态以及指向 Monitor 的指针。 Monitor 监视器：JVM 为每个对象关联一个 Monitor（通过 C++ 实现，类似于操作系统互斥量）。Monitor 内部有三个关键区域： Owner：记录当前持有锁的线程，初始为 null。 EntryList：等待获取锁的线程队列。 WaitSet：已经获得锁但调用 wait() 后释放锁进入等待的线程队列。 加锁过程：线程执行 monitorenter 指令时，检查对象的 Monitor Owner 是否为 null。若为 null，则将 Owner 设为当前线程，锁计数 +1；若不为 null，则进入 EntryList 阻塞等待。 锁升级（重要）：JDK 1.6 后为优化性能，引入了偏向锁 → 轻量级锁 → 重量级锁的升级机制： 偏向锁：只有一个线程反复进入同一同步块时，几乎无开销。 轻量级锁（CAS）：多个线程交替执行但无竞争时，通过 CAS 避免操作系统互斥。 重量级锁：竞争激烈时升级为基于操作系统互斥量的重量级锁，会阻塞并切换线程。 谈一谈 JMM（Java Memory Model） 回答大纲\n抽象模型：线程 - 工作内存 - 主内存 主内存：所有线程共享，存放共享变量 工作内存：每个线程私有，存放用到的变量的副本 八大原子操作：read / load / use / assign / store / write / lock / unlock 正式回答\n**JMM（Java Memory Model，Java 内存模型）**是 Java 虚拟机规范中定义的一套抽象模型，用于屏蔽各种硬件和操作系统的内存访问差异，让 Java 程序在各种平台下都能达到一致的内存访问效果。它定义了线程、工作内存、主内存之间的交互关系。\n主内存：所有线程共享的内存区域，存放所有的共享变量（实例字段、静态字段等）。 工作内存：每个线程私有的内存区域，保存该线程使用到的变量的副本。线程对变量的所有操作（读写、赋值等）都必须在工作内存中进行，不能直接读写主内存中的变量。 八大原子操作（简化）： read：从主内存读取变量到工作内存。 load：将 read 得到的值放入工作内存的变量副本。 use：把工作内存中的变量值传递给执行引擎。 assign：把执行引擎返回的值赋给工作内存中的变量。 store：把工作内存中的变量值传送到主内存。 write：把 store 得到的值写入主内存的变量。 lock / unlock：作用于主内存的变量，锁定或解锁。 三大特性：原子性、可见性、有序性。JMM 通过 synchronized、volatile、final 等关键字围绕这三大特性建立 happens-before 规则，保证多线程的正确性。 CAS 你知道吗 回答大纲\nCompare And Swap，一种无锁并发技术 三个操作数：内存位置 V、预期原值 A、新值 B 底层实现 Unsafe 类 native 修饰，本地方法，由 C/C++ 实现 调用 CPU 硬件指令（如 cmpxchg） 正式回答\nCAS（Compare And Swap，比较并交换）是一种无锁并发技术，用于在多线程环境下实现变量的原子更新。它的核心思想是：内存位置 V 当前的预期值是 A，才将其更新为 B，否则不做任何修改。整个操作是硬件层面的原子操作。\nCAS 的三个关键要素：\n内存位置 V：要修改的变量在内存中的地址。 预期原值 A：调用方认为 V 当前应该的值。 新值 B：希望将 V 更新为的值。 CAS 在 Java 中的实现主要依赖 sun.misc.Unsafe 类（AtomicInteger、AtomicLong 等原子类的底层都依赖它）。Unsafe 中的 compareAndSwapInt 等方法被 native 修饰，会通过 JNI 调用操作系统底层，最终映射到 CPU 的 cmpxchg 指令，由硬件保证原子性。\n优点：无锁，避免了线程阻塞和上下文切换的性能损耗。缺点：\nABA 问题：线程 A 看到 V = A，期间 V 被其他线程改为 B 又改回 A，A 仍会 CAS 成功。可以通过引入版本号（AtomicStampedReference）解决。 循环开销：高竞争下循环 CAS 会浪费 CPU。 只能保证单个变量原子：不能保证代码块原子，需要配合 AtomicReference 等。 乐观锁和悲观锁的区别 回答大纲\n悲观锁：总是假设最坏情况，每次操作都先加锁 synchronized / ReentrantLock 乐观锁：假设数据一般不会冲突，更新时才检查 CAS / 版本号机制 适用场景：竞争激烈用悲观锁；竞争少用乐观锁 正式回答\n**悲观锁（Pessimistic Lock）**总是假设最坏的情况，认为并发操作一定会发生冲突，因此每次操作共享资源时都会先加锁，确保同一时刻只有一个线程能操作，其他线程阻塞等待。Java 中典型的悲观锁实现是 synchronized 和 ReentrantLock。优点是安全性高、不会出现 ABA 等问题；缺点是加解锁开销大、可能导致线程阻塞和上下文切换，并发度低。\n**乐观锁（Optimistic Lock）**则假设数据一般情况下不会发生冲突，因此不会提前加锁，只在更新时检查版本是否被修改过。如果版本（version）或预期值匹配，则更新；否则重试或报错。Java 中的 AtomicInteger、AtomicReference 等原子类以及 ConcurrentHashMap 的部分操作基于 CAS 实现乐观锁。优点是无锁、并发度高；缺点是高竞争下循环重试浪费 CPU，且需要解决 ABA 问题。\n适用场景：\n悲观锁适合写多读少、竞争激烈、临界区执行时间长的场景。 乐观锁适合读多写少、竞争较少的场景，最大化提升吞吐量。 实际应用中两者经常结合使用，例如先乐观尝试，失败后再退化为悲观锁。\n谈一谈对 volatile 的理解 回答大纲\n两大语义 可见性：线程间对共享变量的修改立即可见 有序性：通过内存屏障禁止指令重排序 内存屏障（Memory Barrier） 写屏障：阻止上方其他写操作越过本 volatile 写 读屏障：阻止下方其他读操作越过本 volatile 读 不保证原子性（i++ 不是原子的） 最佳实践：状态标志、双重检查单例的配置/状态字段 正式回答\nvolatile 是 Java 提供的轻量级同步关键字，主要作用有两个：\n保证可见性：当一个线程修改了被 volatile 修饰的共享变量时，其他线程能立即看到最新的值。其实现原理是：对 volatile 变量的写操作会立刻刷回主内存（插入 Store 屏障 + StoreLoad 屏障），读操作会从主内存重新读取最新的值（插入 Load 屏障）。注意：synchronized 和 Lock 也能保证可见性，但开销远大于 volatile。 禁止指令重排序：JVM 为了优化性能会对字节码进行重排序，但对 volatile 变量的读写操作会插入内存屏障，限制重排序。具体规则： 写 volatile 变量：禁止其上方的普通写操作重排到下面。 读 volatile 变量：禁止其下方的普通读操作重排到上面。 需要特别强调的是：volatile 不保证原子性。例如 i++ 实际包含\u0026quot;读 - 加 - 写\u0026quot;三步，并不原子，如需原子计数应使用 AtomicInteger。\n最佳实践：将 volatile 用于线程间共享的状态标志、配置刷新、双重检查单例中的实例引用等场景；不要用它替代 synchronized/Lock，也不能用于需要复合原子操作的场景（如计数器）。\nAQS 回答大纲\n抽象队列同步器（AbstractQueuedSynchronizer） 内部维护一个 FIFO 双向队列（CLH 变体） 核心：state 同步状态 + CAS 设置 state 保证原子性 可实现公平锁 / 非公平锁 公平：按 FIFO 顺序获取 非公平：新线程可与队头线程竞争 正式回答\n**AQS（AbstractQueuedSynchronizer）**是 java.util.concurrent.locks 包下的核心抽象类，是 JUC 中许多同步器（ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 等）的基础框架。\n核心结构： volatile int state：同步状态，是实现锁/同步器的关键。state = 0 表示未占用，state \u0026gt; 0 表示被占用或剩余许可数。 FIFO 双向队列：未抢到资源的线程被打包成 Node 节点进入该队列排队。 核心思想： 资源获取：通过 compareAndSetState（CAS）保证 state 修改的原子性。 资源等待：获取失败的线程会构造 Node 加入队尾，并通过自旋 + LockSupport.park() 阻塞自己。 资源释放：当前持有者释放资源后，会唤醒队头节点（unpark），被唤醒的线程再次尝试 CAS 抢资源。 公平性： 公平锁：严格按照 FIFO 顺序获取，新线程加入队尾。 非公平锁：新线程可直接与队头线程竞争（ReentrantLock 默认）。 资源共享模式：AQS 支持独占模式（如 ReentrantLock）和共享模式（如 Semaphore、CountDownLatch），分别在 tryAcquire/tryRelease 和 tryAcquireShared/tryReleaseShared 中实现。 ReentrantLock 的实现原理 回答大纲\n可重入锁：一个线程可多次获取同一把锁 底层基于 AQS + CAS 特点 可中断 可设置超时时间 可设置公平锁 支持多个条件变量 支持重入 正式回答\nReentrantLock 是 JUC 中常用的可重入互斥锁，底层基于 AQS + CAS 实现。它通过 state 记录重入次数：每次重入 state + 1，释放时 state - 1，直到 state = 0 才真正释放锁。\n主要特点：\n可重入：同一个线程可以多次获取同一把锁（递归调用同一方法不会死锁），通过记录线程持有者实现。 可中断：lockInterruptibly() 让线程在等待锁时能响应中断。 可设置超时时间：tryLock(long timeout, TimeUnit unit) 等待指定时间后无论是否获得锁都返回，避免无限阻塞。 可设置公平锁：通过 new ReentrantLock(true) 构造公平锁，严格按 FIFO 顺序获取；默认是非公平锁。 支持多个条件变量：Condition 比 synchronized + wait/notify 更灵活，可以精确唤醒特定条件队列上的线程（signal()/signalAll()）。 实现原理：在 AQS 的 tryAcquire 中，先用 CAS 修改 state，再判断当前线程是否已经持有，是则允许重入；tryRelease 时递减 state，直到归零释放。\nsynchronized 和 Lock 有什么区别？ 回答大纲\nsynchronized 关键字，基于 JVM Monitor，由 C++ 实现 自动加解锁，不可中断 悲观锁 Lock（ReentrantLock） 基于 AQS + CAS 的 API 类 可手动加解锁、公平/非公平、响应中断 竞争激烈时性能更好 正式回答\n对比维度 synchronized Lock（ReentrantLock） 类型 关键字 类（API） 实现 JVM Monitor（C++） AQS + CAS（Java） 加解锁 自动（进入/退出同步块） 手动 lock()/unlock() 公平性 仅非公平 可选公平 / 非公平 可中断 不支持 lockInterruptibly() 超时 不支持 tryLock(timeout, unit) 多条件 单一 wait/notify 多个 Condition 性能 JDK 1.6 后大幅优化，常规场景接近 Lock 竞争激烈时更优 ReentrantLock 比 synchronized 更灵活，但也意味着必须在 finally 中释放锁避免死锁。JDK 1.6 之后 synchronized 引入了偏向锁、轻量级锁、锁消除等优化，两者性能差距已经很小。日常推荐使用 synchronized（语法简洁、不会忘记释放）；只有在需要公平锁、可中断、超时、多条件等高级特性时使用 ReentrantLock。\n死锁产生的条件是什么？ 回答大纲\n四个必要条件（同时满足时才会死锁） 互斥：资源同一时刻只能被一个线程占用 占有并等待：线程持有资源的同时等待其他资源 不可剥夺：资源只能由线程主动释放，不能被强制剥夺 循环等待：线程之间形成等待环路 破坏任意一个条件即可避免死锁 正式回答\n死锁是指两个或两个以上的线程在执行过程中，因互相持有对方需要的资源而导致的相互等待，程序无法继续推进的状态。死锁的产生需要同时满足以下四个必要条件：\n互斥条件：资源一次只能被一个线程占用。 占有并等待条件：线程在持有至少一个资源的同时，去请求其他被占用的资源。 不可剥夺条件：线程已经获得的资源，在未使用完之前，不能被强制剥夺，只能由持有线程主动释放。 循环等待条件：存在一条线程资源等待环路，例如线程 A 等线程 B 持有的资源，线程 B 又等线程 A 持有的资源。 只要破坏以上任意一个条件，就可以避免死锁。实际开发中常用的做法是破坏循环等待条件：所有线程按相同顺序申请锁，例如总是先锁 A 再锁 B。\n如何进行死锁诊断？ 回答大纲\n命令行工具 jps 列出 Java 进程 jstack \u0026lt;pid\u0026gt; 查看线程堆栈，定位死锁 图形化工具 jconsole JVisualVM / VisualVM 正式回答\n排查死锁主要有以下几种手段：\njps：JDK 自带命令，列出当前机器上所有 Java 进程及其 PID。 jstack \u0026lt;pid\u0026gt;：查看指定进程中各线程的堆栈信息。jstack 会自动检测是否存在死锁，并提示\u0026quot;Found one Java-level deadlock\u0026quot;以及死锁线程、持有的锁和等待的锁。 jconsole：JDK 自带的 GUI 监控工具，连接目标进程后切换到\u0026quot;线程\u0026quot;页签，可以直观看到死锁线程和锁持有情况。 VisualVM：功能更强大的 GUI 工具，能查看线程运行状态、堆栈、CPU 占用、内存等，在线程页签点击\u0026quot;检测死锁\u0026quot;按钮即可识别死锁。 排查思路一般是：先 jstack 看到死锁线程，再回到代码里结合日志确认具体场景，然后检查锁申请顺序或者是否存在锁内调用外部方法等问题，针对性修复（如调整为统一加锁顺序、使用 tryLock 设置超时等）。\n聊一聊 ConcurrentHashMap 回答大纲\nJDK 1.7：分段锁（Segment 数组 + HashEntry） JDK 1.8：CAS + synchronized，锁粒度更细（锁住单个桶位的头节点） 读操作无锁；写操作根据桶位状态选择 CAS 或 synchronized size 通过 baseCount + CounterCell[] 求和 正式回答\nConcurrentHashMap 是 JUC 下的线程安全哈希表实现，替代了同步开销大的 Hashtable 和 Collections.synchronizedMap。它在 JDK 1.7 和 1.8 中采用了完全不同的实现思路。\nJDK 1.7：分段锁（Segment）\n内部维护一个 Segment[] 数组，每个 Segment 继承自 ReentrantLock，相当于一小段 HashMap。 每个 Segment 内部是一个独立的哈希表（HashEntry[]），同一时刻多个线程可以分别抢占不同 Segment 的锁并发写入。 默认并发度（concurrencyLevel）为 16，即最多支持 16 个线程同时写入。读操作不加锁（volatile 保证可见性）。 缺点：分段粒度仍然较粗，扩容时影响整个 Segment，且查询时需要两次哈希。 JDK 1.8：CAS + synchronized + 红黑树\n取消 Segment，底层结构与 HashMap 类似（Node[] + 链表/红黑树），并发度由 Node 粒度决定。 插入流程： 桶位为空：用 CAS 插入新节点，避免加锁。 桶位不为空：用 synchronized 锁住该桶位的头节点（链表头/树根），锁粒度极细。 链表长度 ≥ 8 且数组 ≥ 64 时树化。 读操作无锁：通过 volatile 保证 Node 中 val 和 next 的可见性。 统计 size：不维护全局 size 变量，而是通过 baseCount 加上 CounterCell[] 累加，最后 sumCount() 求和，避免 size 竞争。 总体上 JDK 1.8 的实现更轻量，查询遍历性能更优，是面试中应重点掌握的版本。\n导致并发程序出现问题的根本原因是什么（Java 如何保证多线程的安全） 回答大纲\n并发编程三大特性 原子性：一个操作或多个操作要么全部执行且不被中断，要么全部不执行 可见性：一个线程修改了共享变量，其他线程能立即看到 有序性：程序执行的顺序按照代码的先后顺序执行（防止指令重排序） Java 通过 synchronized / Lock（原子性、可见性）、volatile（可见性、有序性）、final、happens-before 规则等保证 正式回答\n并发程序出现问题的根本原因是三大特性被破坏：\n原子性（Atomicity）：一个操作或一系列操作要么全部执行，要么全部不执行，不能被中断。例如 i++ 在字节码层面实际是 4 步操作，非原子性导致丢失更新。Java 通过 synchronized、Lock、AtomicXxx（CAS）等保证。 可见性（Visibility）：一个线程修改了共享变量的值，其他线程能立即看到该修改。由于每个线程有工作内存，可能存在本地副本未及时同步到主内存的情况，导致可见性问题。Java 通过 volatile、synchronized、Lock、final 等保证。 有序性（Ordering）：程序执行的顺序按代码的先后顺序执行。但 JVM/CPU 可能为优化进行指令重排序，从而破坏有序性。Java 通过 volatile、synchronized、Lock 以及 happens-before 原则保证。 Java 内存模型（JMM）围绕这三大特性建立了一套 happens-before 规则，只要操作 A happens-before 操作 B，那么 A 的结果就对 B 可见。\n线程池的核心参数？ 回答大纲\n7 个核心参数（ThreadPoolExecutor 构造器） corePoolSize：核心线程数 maximumPoolSize：最大线程数 keepAliveTime：救急线程空闲存活时间 unit：时间单位 workQueue：工作队列 threadFactory：线程工厂 handler：拒绝策略 正式回答\nThreadPoolExecutor 是线程池的核心实现类，它的构造器有 7 个参数：\ncorePoolSize（核心线程数）：线程池中长期保留的线程数，即使这些线程处于空闲状态，也不会被回收（默认 allowCoreThreadTimeOut=false）。 maximumPoolSize（最大线程数）：线程池允许创建的最大线程数，等于核心线程数 + 救急线程数。当工作队列已满时，会创建救急线程执行任务，救急线程在空闲超过 keepAliveTime 后会被回收。 keepAliveTime（存活时间）：救急线程的最大空闲时间，超过该时间会被回收。 unit（时间单位）：keepAliveTime 的单位，如 TimeUnit.SECONDS。 workQueue（工作队列）：用于保存等待执行任务的阻塞队列，常见有 ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue、DelayedWorkQueue。 threadFactory（线程工厂）：用于创建线程的工厂，可以设置线程名、守护线程、异常处理器等，便于排查问题。 handler（拒绝策略）：当线程池和工作队列都已满，无法再接受新任务时的处理策略，JDK 内置 4 种：抛异常 AbortPolicy（默认）、让调用者执行 CallerRunsPolicy、丢弃 DiscardPolicy、丢弃最早任务 DiscardOldestPolicy。 线程池的执行原理知道吗？ 回答大纲\n提交任务 → 核心线程是否已满 未满：交给核心线程执行 已满：进入工作队列等待 队列未满：入队等待 队列已满：创建救急线程执行 救急线程达到 max：执行拒绝策略 正式回答\n当向线程池提交一个任务时，线程池按以下顺序处理：\n判断核心线程：当前工作线程数是否小于 corePoolSize？ 是：创建核心线程执行任务（即使有空闲核心线程也会创建新线程）。 否：进入下一步。 判断工作队列：尝试将任务放入 workQueue。 成功入队：等待核心线程空闲时取出执行。 入队失败（队列已满）：进入下一步。 判断最大线程：当前工作线程数是否小于 maximumPoolSize？ 是：创建救急线程立即执行该任务。 否：进入下一步。 执行拒绝策略：根据 handler 处理新任务（抛异常、调用者执行、丢弃等）。 此外，线程空闲时回收机制：核心线程默认不会被回收；救急线程在空闲超过 keepAliveTime 后会被回收（如果设置了 allowCoreThreadTimeOut(true)，核心线程也会被回收）。\n线程池中常见的阻塞队列 回答大纲\nArrayBlockingQueue：数组、有界、FIFO、一把锁 LinkedBlockingQueue：链表、可有界、FIFO、两把锁 SynchronousQueue：不存储元素、只做中转 DelayedWorkQueue：按延迟时间排序，调度型 正式回答\n线程池的工作队列都是 BlockingQueue 的实现，不同队列对线程池的行为影响很大：\nArrayBlockingQueue：基于数组实现，有界队列，FIFO 顺序。使用一把锁（ReentrantLock），并发性能相对一般，但内存紧凑。 LinkedBlockingQueue：基于链表实现，默认无界（Integer.MAX_VALUE），也可在构造时指定容量。FIFO 顺序。使用两把锁（takeLock 和 putLock），并发性能优于 ArrayBlockingQueue。注意：默认无界可能导致 OOM。 SynchronousQueue：不存储元素的阻塞队列，每次插入必须等待一个对应的取出操作。适合任务密集且执行快的场景，如 CachedThreadPool。 DelayedWorkQueue：按任务的延迟时间排序的堆结构，用于 ScheduledThreadPoolExecutor，能按延迟或周期执行任务。 PriorityBlockingQueue：带优先级的无界队列，按比较器排序。 LinkedBlockingDeque：双端队列，支持从两端操作。 如何确定核心线程数？ 回答大纲\nCPU 密集型：N + 1，减少上下文切换 IO 密集型：2N + 1，最大化利用线程（等待 IO 时 CPU 不忙） 实际还需要根据压测和业务调整 正式回答\n核心线程数的设置没有标准答案，需要根据任务类型来估算：\nCPU 密集型任务：CPU 使用率高，几乎没有阻塞。 推荐公式：核心线程数 = CPU 核数 + 1。 多出的 1 个线程是为了在某个线程偶发缺页中断或其他小阻塞时，CPU 仍能保持繁忙。 I/O 密集型任务：包含大量阻塞操作（数据库、HTTP、网络、文件 IO 等）。 推荐公式：核心线程数 = CPU 核数 × 2 + 1，也可以进一步按 IO 等待时间与 CPU 计算时间的比例放大。 核心思想：阻塞时 CPU 闲置，配置更多线程可以让 CPU 一直有事可做。 混合型任务：可以拆分为 CPU 密集阶段和 IO 密集阶段，分别交给不同线程池处理。 但是公式只是起点，实际生产环境要结合压测（JMeter、wrk、ab）和监控（JDK 自带的线程池指标）进行调优，找到平衡点。\n线程池的种类有哪些？ 回答大纲\nFixedThreadPool：固定线程数 SingleThreadExecutor：单线程 CachedThreadPool：可缓存，无核心线程 ScheduledThreadPool：支持延迟和周期执行 WorkStealingPool：ForkJoinPool，工作窃取 正式回答\nExecutors 工厂类提供了几种常见的线程池。但生产环境不推荐直接使用 Executors，因为容易产生 OOM 问题，应当手动创建 ThreadPoolExecutor。\nnewFixedThreadPool(n)：固定大小的线程池。 核心线程数 = 最大线程数 = n。 工作队列为无界的 LinkedBlockingQueue。 适用场景：已知任务量、任务耗时相对固定的长任务（如数据导入、报表生成）。 newSingleThreadExecutor()：单线程线程池。 保证所有任务按提交顺序串行执行。 适用场景：需要保证顺序执行、避免并发问题的任务流（如日志写入）。 newCachedThreadPool()：可缓存的线程池。 没有核心线程，只有救急线程（60 秒存活时间）。 工作队列为 SynchronousQueue（不存储元素）。 适用场景：任务数密集但每个任务执行时间短的场景，大量短任务容易创建大量线程，注意 CPU 占用和 OOM。 newScheduledThreadPool(n)：支持延迟和周期执行的线程池。 内部基于 ScheduledThreadPoolExecutor，使用 DelayedWorkQueue。 适用场景：定时任务、心跳检测、周期统计。 newWorkStealingPool()：基于 ForkJoinPool 的工作窃取线程池，适合大任务并行拆分（如并行流 parallelStream 的默认实现）。 为什么不建议使用 Executors 创建线程池？ 回答大纲\n主要原因：可能 OOM newFixedThreadPool / newSingleThreadExecutor 使用无界 LinkedBlockingQueue，任务堆积可能导致 OOM newCachedThreadPool 允许创建 Integer.MAX_VALUE 个线程，线程数过多导致 OOM 建议手动创建 ThreadPoolExecutor，明确 7 个参数 正式回答\n阿里《Java 开发手册》等业界规范明确指出禁止使用 Executors 创建线程池，主要原因在于容易引发 OOM（OutOfMemoryError）：\nnewFixedThreadPool(int) 和 newSingleThreadExecutor() 的工作队列都是 LinkedBlockingQueue，而默认容量为 Integer.MAX_VALUE，可视为无界队列。如果任务提交速度持续大于处理速度，队列会不断堆积，最终撑爆堆内存，产生 OOM。 newCachedThreadPool() 允许创建的线程数上限为 Integer.MAX_VALUE。在突发大量任务时，会瞬时创建大量线程，每个线程都会占用一定栈空间（默认 1MB），叠加后引发 OOM。 正确做法：根据业务场景手动创建 ThreadPoolExecutor，明确指定核心线程数、最大线程数、队列容量、拒绝策略等参数，使线程池的行为可预测、可监控。\n你们项目中哪里使用了线程池？ 回答大纲\n常用技术 CountDownLatch：主线程等待多个子任务完成 使用场景 ES 数据批量导入（数据汇总） 将串行业务改造为并行（数据汇总、报表汇总） 异步调用 异步通知 正式回答\n在生产项目中，线程池主要用于解耦异步执行和提升并发处理能力。常见场景包括：\n异步调用：用户操作触发的非关键路径（如发送通知、记录日志、更新缓存）通过 @Async 或手动 submit 提交到线程池，避免阻塞主流程。 数据汇总 / 报表统计：将原本串行执行的多个耗时统计任务，并行提交到线程池，最后合并结果（通常配合 CountDownLatch 或 CompletableFuture.allOf() 等待全部完成）。 ES 数据批量导入：从数据库读取大量数据后，分批次提交到线程池并发写入 ES，配合 ThreadPoolExecutor 限制并发度保护 ES。 批量任务并行化：例如批量发送短信、批量调用三方接口，将 N 个独立子任务并行处理后再聚合。 定时任务 / 心跳检测：使用 ScheduledThreadPool 执行周期任务。 常用工具类包括 CountDownLatch（协调多任务合并）、CompletableFuture（组合多个异步任务）等。\n如何控制某个方法允许并发访问线程的数量？ 回答大纲\n使用 Semaphore（信号量） 构造时传入许可数 permits acquire() 获取许可，release() 释放许可 也可使用 Hystrix / Resilience4j 等限流框架 正式回答\n如果希望控制同时访问某段代码的线程数量，可以使用 JUC 的 Semaphore（信号量）：\nSemaphore semaphore = new Semaphore(3); // 最多 3 个线程同时执行 try { semaphore.acquire(); // 核心业务逻辑 } finally { semaphore.release(); } Semaphore 内部基于 AQS 实现：\n构造时传入许可数 permits，表示允许的最大并发线程数。 acquire()：获取许可，如果许可数为 0 则阻塞（或响应中断、带超时）。 release()：释放许可，把许可数加 1，唤醒等待的线程。 除了 Semaphore，还可以：\n使用 Guava RateLimiter 实现 QPS 级别的令牌桶限流。 在分布式场景下使用 Redis + Lua / Sentinel / Resilience4j 实现分布式限流。 在网关层使用 Nginx / Sentinel 做接口级限流。 谈谈你对 ThreadLocal 的理解 回答大纲\n每个线程持有一份独立的变量副本，互不干扰 底层数据结构：Thread.threadLocals，类型为 ThreadLocalMap 内存泄漏风险：ThreadLocalMap 中 Entry 的 Key 是弱引用，Value 是强引用 最佳实践：使用完毕后调用 remove() 正式回答\nThreadLocal 是 JDK 提供的线程本地变量工具，能为每个线程提供独立的变量副本，使多个线程之间互不干扰。常见用法：\nprivate static final ThreadLocal\u0026lt;User\u0026gt; USER_HOLDER = new ThreadLocal\u0026lt;\u0026gt;(); USER_HOLDER.set(currentUser); // 每个线程都有一份 User u = USER_HOLDER.get(); // 取自己线程的副本 USER_HOLDER.remove(); // 用完清理 底层原理：\n每个 Thread 对象内部都有一个 ThreadLocalMap threadLocals，它是 ThreadLocal 的静态内部类。 set(value) 时，以当前 ThreadLocal 为 key、value 为值写入当前线程的 ThreadLocalMap。 get() 时，在当前线程的 ThreadLocalMap 中查找对应的 entry。 ThreadLocalMap 的 Entry 继承自 WeakReference\u0026lt;ThreadLocal\u0026gt;，因此 key 是弱引用，value 是强引用。 内存泄漏 OOM 风险：当 ThreadLocal 变量被回收后（key 变 null），如果线程一直存活（典型场景是线程池中的核心线程），那么 ThreadLocalMap 中就会出现 key=null 但 value 仍强引用的 entry。这部分 value 既无法被访问也无法被回收，最终可能导致内存泄漏。\n最佳实践：每次使用完 ThreadLocal 后，主动调用 remove() 方法清理 entry，特别在线程池场景下尤其重要。这也是阿里等公司代码规范的强制要求。\nJVM 组成 什么是 JVM？ 回答大纲\nJava 虚拟机，Java 二进制字节码的运行环境 负责加载字节码并解释/编译执行 提供自动内存管理和垃圾回收 主要组成部分：类加载器、运行时数据区、执行引擎、本地方法接口、垃圾收集器 正式回答\n**JVM（Java Virtual Machine，Java 虚拟机）**是 Java 平台的基石，是运行 Java **字节码（.class）**的虚拟计算机。它主要完成以下工作：\n字节码加载：通过类加载器子系统将 .class 文件加载到运行时数据区。 字节码执行：通过执行引擎中的解释器逐条解释执行，或通过 JIT（即时编译器）将热点代码编译成本地机器码提升性能。 自动内存管理：在堆内存中为对象分配空间，并通过垃圾回收器自动回收不再使用的对象，避免手动内存管理的风险。 跨平台性：JVM 针对不同操作系统有不同的实现（Windows、Linux、macOS），同一份字节码可以在任何安装了 JVM 的机器上运行，实现了\u0026quot;Write Once, Run Anywhere\u0026quot;。 什么是程序计数器？ 回答大纲\n线程私有，生命周期与线程相同 存储当前线程执行的字节码指令地址（行号） 唯一一个不会出现 OOM 的内存区域 Native 方法时计数器为 undefined 正式回答\n程序计数器（Program Counter Register，PC Register）是 JVM 运行时数据区的一部分，是线程私有的内存区域。每一个线程都有自己的程序计数器，彼此独立。\n作用：在当前线程执行的字节码中，记录下一条要执行的指令的地址（即行号）。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。 唯一一个不会出现 OOM 的内存区域：因为它存储的是指令地址，不需要 GC，也不需要扩展，所以是 JVM 规范中唯一不会抛出 OutOfMemoryError 的区域。 Native 方法：如果线程执行的是 Native 方法，程序计数器的值为 undefined，因为本地方法由操作系统调度，不由 JVM 字节码执行。 线程上下文切换：当线程被挂起再次切回时，需要通过程序计数器恢复到正确的执行位置，因此每个线程都有自己的 PC。 可以用 javap -v Test.class 查看 class 文件，能在 Code 区看到每一行字节码对应程序计数器的偏移值。\n详细介绍 Java 的堆 回答大纲\n线程共享的区域 存放对象实例和数组 Java 7 与 Java 8 内存结构对比 Java 7：新生代 + 老年代 + 永久代（PermGen） Java 8：新生代 + 老年代 + 元空间（Metaspace），元空间使用本地内存 堆是 GC 主要管理区域 正式回答\n堆（Heap）是 JVM 运行时数据区中线程共享的一块内存区域，是 JVM 所管理的内存中最大的一块。\n存储内容：几乎所有的对象实例和数组都在堆上分配（部分逃逸分析优化后可在栈上分配）。\nGC 主战场：堆是垃圾回收器主要管理的区域，因此也被称为\u0026quot;GC 堆\u0026quot;。现代 GC（如 G1、ZGC）几乎所有的回收算法都围绕堆展开。\n分代划分：基于对象生命周期假设，堆被划分为：\n新生代（Young Gen）：刚创建的对象，新生代又分为 Eden 区和两个 Survivor 区（S0、S1）。 老年代（Old Gen）：经过多次 GC 仍存活的对象，会被晋升到这里。 Java 7 vs Java 8 内存结构对比：\n版本 主要划分 Java 7 新生代 + 老年代 + 永久代（PermGen）：存放类元数据 Java 8 新生代 + 老年代 + 元空间（Metaspace）：使用本地内存 将永久代改为元空间的主要原因是：永久代大小受 JVM 参数限制（-XX:MaxPermSize），容易出现 java.lang.OutOfMemoryError: PermGen space；而元空间使用本地内存（默认仅受系统可用内存限制），可以显著降低 OOM 概率，且便于调优。\n什么是虚拟机栈？ 回答大纲\n线程私有，生命周期与线程相同 每个线程运行时需要的内存，称为虚拟机栈 由多个**栈帧（Stack Frame）**组成 活动栈帧：当前正在执行的方法对应的栈帧 每个栈帧包含：局部变量表、操作数栈、动态链接、方法返回地址 正式回答\n**虚拟机栈（VM Stack）**是 JVM 运行时数据区的一部分，线程私有，生命周期与线程相同。\n栈帧（Stack Frame）：每个方法执行时，JVM 都会创建一个栈帧并压入栈中。方法执行完毕后栈帧出栈。 栈帧的组成： 局部变量表：存放方法参数和方法内部定义的局部变量（包括基本类型和对象引用）。 操作数栈：方法执行过程中用于数据传递和计算的临时区域。 动态链接：指向运行时常量池中该方法的引用，支持多态调用。 方法返回地址：方法正常返回或异常退出后，回到调用者的位置。 活动栈帧：栈顶的栈帧对应的是正在执行的（最里层）方法，又称\u0026quot;当前栈帧\u0026quot;。 栈的运作：方法调用 → 栈帧入栈；方法返回 → 栈帧出栈（不管正常 return 还是抛异常）。 线程安全：由于栈是线程私有的，栈帧内部的局部变量天生线程安全，无需额外同步。 垃圾回收是否涉及栈内存？ 回答大纲\n不涉及 栈内存由方法调用和返回自动管理，不需要 GC 介入 GC 指的是堆内存的回收 正式回答\n垃圾回收（GC）不涉及栈内存。\n因为虚拟机栈描述的是 Java 方法执行的内存模型，栈帧的入栈和出栈是严格遵循\u0026quot;先进后出\u0026quot;原则的：方法调用时入栈，方法返回时出栈（无论是正常返回还是抛异常）。栈帧的整个生命周期与方法的执行流程绑定，在编译期就能确定大小（局部变量表、操作数栈深度都可知）。所以栈内存不需要 GC 介入，也不需要考虑复杂的内存回收问题。\nGC 的主要对象是堆内存，尤其是新生代；其他区域如方法区/元空间也会有少量 GC。栈内存溢出（StackOverflowError）和堆内存溢出（OOM）是性质完全不同的两类异常。\n栈内存分配越大越好吗？ 回答大纲\n不是 栈内存越大，可支持的递归/调用深度越大，但单个线程占用内存也越大 多线程场景下总内存会膨胀，可能挤占堆内存 一般默认 -Xss 1024k 就足够；推荐根据实际调用深度设置 正式回答\n并不是越大越好。\n栈内存由 -Xss 参数控制（例如 -Xss1024k）。盲目调大栈内存有两个潜在问题：\n单线程占用增加：每个线程都会分配独立的栈空间。栈越大，可支持的调用深度（递归）越深，但单线程占用的内存也越多。 总内存膨胀，挤占堆内存：操作系统能创建的线程数受总内存限制。如果每个线程栈都是 1MB，1000 个线程就占用 1GB，挤占了堆的可用内存，反而可能更容易引起 OOM。 收益递减：大部分应用的调用深度不过几十层到几百层，1MB 足够；过度调大栈并不能换来等比例的调用深度提升。 最佳实践：保持默认值（一般 512KB ~ 1MB 即可），只有在确实遇到 StackOverflowError（例如复杂递归）时才有针对性地调大，并同步评估对线程数和堆内存的影响。\n方法内的局部变量是否线程安全？ 回答大纲\n是线程安全的 局部变量保存在每个线程的栈帧中，是线程私有的 注意：局部变量引用对象本身如果在方法外被共享，则对象操作不线程安全 注意：将局部变量返回出去后可能被外部多线程访问，不再线程安全 正式回答\n方法内的局部变量本身是线程安全的，因为：\n栈帧私有：每个线程调用方法时都会在自己的虚拟机栈中创建一个独立的栈帧，局部变量保存在各自栈帧的局部变量表中，天然不与其他线程共享。 不并发：线程 A 的局部变量不会影响线程 B 的同名局部变量。 但是需要注意两种\u0026quot;反例\u0026quot;：\n返回局部变量：如果方法返回了一个局部对象引用，外部多个线程可能持有同一引用并进行操作，那么对对象成员的操作就不再线程安全，需要额外同步。 对象逃逸：局部变量指向的对象如果在方法外还有其他引用（例如通过参数传入），那么对象本身可能被多线程访问，操作其字段也要考虑同步。 简而言之：局部变量本身是线程安全的，但局部变量引用的对象是否线程安全，取决于它的作用域是否被多个线程共享。\n方法外的局部变量是否线程安全？ （说明：Java 中没有严格意义上的\u0026quot;方法外局部变量\u0026quot;。这里通常指成员变量/实例变量。）\n回答大纲\n这里的\u0026quot;方法外局部变量\u0026quot;通常指成员变量（实例变量 / 静态变量） 成员变量保存在堆中，是线程共享的 多线程同时读写同一个成员变量，需要同步保证线程安全 正式回答\nJava 中\u0026quot;方法外\u0026quot;的变量指成员变量（实例字段/静态字段），它们与局部变量有本质区别：\n成员变量存储在堆中（实例字段随对象在堆中，静态字段在方法区/元空间），是所有线程共享的。 如果多个线程同时读写同一个成员变量，就会产生竞态条件（Race Condition），必须通过 synchronized、Lock、volatile、AtomicXxx 等手段保证线程安全。 每条规则都有例外：如果对象本身是线程封闭的（例如只在单个线程中使用），那么它的成员变量也不存在多线程访问问题，天然安全。例如 ThreadLocal 把对象封闭在线程内，或 Swing 的事件派发线程（EDT）。 什么情况下会导致栈内存溢出？ 回答大纲\n抛出 java.lang.StackOverflowError 常见场景 递归调用过深（最常见） 栈帧过大（极少见） 可通过 -Xss 调大栈内存缓解，但不能根治 正式回答\n当线程请求的栈深度超过了 JVM 允许的最大深度，就会抛出 java.lang.StackOverflowError。\n最常见：递归调用过深。例如没有正确收敛条件的递归，会无限压栈直至栈耗尽。在实际项目中，常常是因为循环引用结构的 toString / hashCode 触发（如 A → B → A）。 栈帧过大：单个栈帧的局部变量表非常大或操作数栈深度非常深（极端情况）。现代 JVM 编译期基本不会出现。 线程栈设置过小：操作系统给每个线程分配的栈内存过小，跑深度递归时容易溢出。 解决方法：\n排查并修复递归逻辑（如加上正确的收敛条件）。 用循环 + 栈（Stack）或尾递归优化代替深递归。 必要时通过 -Xss 调大每个线程的栈空间，但不要盲目调大。 使用 @SneakyThrows 之类的工具只能掩盖错误，并不能解决根本问题。 堆和栈的区别？ 回答大纲\n存储内容：堆存对象实例/数组；栈存栈帧（局部变量、操作数栈等） 线程私有性：堆线程共享；栈线程私有 异常：堆 OOM；栈 StackOverflowError 空间大小：堆通常很大（GB 级）；栈相对较小（MB 级 / 线程） GC：堆是 GC 主战场；栈不需要 GC 正式回答\n对比维度 堆（Heap） 虚拟机栈（VM Stack） 线程私有 线程共享 线程私有 存储内容 对象实例、数组 栈帧：局部变量、操作数栈、动态链接、返回地址 空间大小 通常较大（GB 级），由 -Xms / -Xmx 控制 较小（默认 1MB 左右），由 -Xss 控制 异常 OutOfMemoryError StackOverflowError GC 主要 GC 区域 不参与 GC 生命周期 JVM 启动时创建，关闭时释放 与线程生命周期相同 性能关注 关注 GC 算法、内存分代、对象分配率 关注调用深度、栈帧大小 实际开发中常见的内存问题主要是：堆 OOM（对象过多或内存泄漏）和栈 StackOverflow（递归过深）。\n能不能解释一下方法区？ 回答大纲\n线程共享的内存区域 存储已被加载的类信息、字段、方法、常量、静态变量 Java 8 之前叫永久代（PermGen），使用 JVM 内存 Java 8 之后改为元空间（Metaspace），使用本地内存 是 JVM 规范中的概念，JVM 各版本实现不同 正式回答\n方法区（Method Area）是 JVM 规范定义的运行时数据区之一，是线程共享的内存区域，用于存储已被虚拟机加载的类信息、字段、方法、常量、静态变量、即时编译器编译后的代码缓存等数据。\nJVM 规范 vs 实现：方法区是 JVM 规范中定义的概念，具体的实现因 JVM 版本而异。 Java 7 之前：永久代（PermGen）：HotSpot 用一块 JVM 堆内的内存实现方法区，通过 -XX:PermSize / -XX:MaxPermSize 设置大小。容易因为动态加载大量类（如 JSP、动态代理、OSGi）而出现 PermGen OOM。 Java 8 之后：元空间（Metaspace）：HotSpot 把方法区实现改为元空间，使用本地内存（Native Memory）。默认情况下只受系统可用内存限制，极大降低了 OOM 概率。元空间通过 -XX:MetaspaceSize / -XX:MaxMetaspaceSize 调优。 演进的好处：合并类元数据和堆，独立管理元数据；为后续支持更大动态类的项目（如 GraalVM）做准备。 graph TB subgraph JVM[\"🖥️ JVM 运行时数据区\"] direction TB subgraph Thread_Shared[\"🧵 线程共享区\"] direction LR Heap[\"🗑️ 堆内存(Heap)存放：对象实例、数组GC 主要回收区\"] Meta[\"📚 元空间(Metaspace)存放：类元数据、方法信息常量池、JIT编译代码使用本地内存\"] end subgraph Thread_Private[\"👤 线程私有区\"] direction LR Stack[\"📦 Java虚拟机栈(VM Stack)存放：栈帧（局部变量表、操作数栈、方法出口）\"] NativeStack[\"⚙️ 本地方法栈(Native Stack)存放：native方法调用信息\"] PC[\"📟 程序计数器(PC Register)存放：当前执行的字节码指令地址\"] end DirectMem[\"💾 直接内存(Direct Memory)NIO 使用的堆外内存\"] end ClassLoader[\"📂 类加载器(ClassLoader)\"] --\u003e Meta Meta --\u003e|类元数据引用| Heap Heap --\u003e|对象引用| Stack NativeStack --\u003e|调用| NativeMethod[\"🛠️ 本地方法库\"] DirectMem --\u003e|缓冲区| Heap 解释一下常量池 回答大纲\n分为运行时常量池和字符串常量池 运行时常量池：每个类一份，存储字面量、符号引用 字符串常量池：全局共享，存放字符串字面量，避免重复创建 字符串常量池在 Java 7 移到堆中，Java 8 仍在堆中 intern() 方法可以把堆中字符串加入常量池 正式回答\nJVM 的常量池主要分两类：\n运行时常量池（Runtime Constant Pool）：每个类被加载后，class 文件中的\u0026quot;常量池表\u0026quot;会被加载到运行时常量池中（属于方法区/元空间）。它存放编译期生成的各种字面量（字符串、final 常量）和符号引用（类、方法、字段的符号）。运行期间也可以通过 ldc / invokedynamic 把新的常量放入池中（如 lambda 表达式生成的引导方法）。 字符串常量池（String Pool）：是运行时常量池的一部分，专门用于存放字符串字面量（如 \u0026quot;hello\u0026quot;）。在 Java 7 之前位于方法区（PermGen），在 Java 7 及之后移到堆中。作用是避免重复创建相同字符串，因为 String 重写了 equals 和 hashCode，可以用 HashMap 结构快速查重。 String.intern()：可以把堆中的 String 对象加入字符串常量池。如果池中已有相同内容的字符串则返回池中的引用；否则把当前引用加入池中。这正是 \u0026quot;a\u0026quot; + \u0026quot;b\u0026quot; == \u0026quot;ab\u0026quot; 在某些情况下为 true 的原因。 注意：运行时常量池与字符串常量池不是同一个东西。前者是类级别的元数据概念，后者是 JVM 共享的字符串表。\n你听过直接内存吗？ 回答大纲\n直接内存不属于 JVM 运行时数据区 由操作系统管理 通过 NIO 的 ByteBuffer.allocateDirect 分配 适合大文件读写、网络数据传输 可通过 -XX:MaxDirectMemorySize 配置上限 正式回答\n直接内存（Direct Memory）是 JVM 之外的、由操作系统直接管理的内存区域，并不属于 JVM 运行时数据区（Java 规范之外的部分）。\n分配方式：在 JDK 的 NIO 中，可以通过 ByteBuffer.allocateDirect() 申请直接内存，本质上是在 JVM 堆外申请一块 native 内存。 NIO vs BIO：传统 BIO 的 FileInputStream/FileOutputStream 等使用堆内字节数组作为缓冲区，读写文件时需要先把数据从磁盘拷贝到内核缓冲区，再拷贝到 JVM 堆；如果用户空间再进行二次处理，可能还有一次到用户缓冲区的拷贝。NIO 的直接内存模式（FileChannel + DirectByteBuffer）能减少一次用户空间到内核空间的拷贝，提高 I/O 效率。 典型案例 - 文件拷贝： 用 FileInputStream + ByteArrayOutputStream 拷贝：磁盘 → 内核缓冲区 → JVM 堆 → 内核缓冲区 → 磁盘。 用 NIO FileChannel + DirectByteBuffer 拷贝：磁盘 → 内核缓冲区 → 磁盘，减少中间两次 JVM 堆缓冲的拷贝。 配置：可通过 -XX:MaxDirectMemorySize 配置上限。超出系统可用内存时也会 OOM，但比堆 OOM 排查更困难。 回收：DirectByteBuffer 是虚引用 + Cleaner 机制回收，使用时应当注意显式释放或控制规模。 JVM 类加载器 什么是类加载器，类加载器有哪些？ 回答大纲\n类加载器负责将 .class 文件加载到 JVM Java 8 及以前四类加载器 BootstrapClassLoader（启动类加载器） ExtClassLoader（扩展类加载器） AppClassLoader（应用类加载器） 自定义类加载器 父子关系通过 getParent() 体现（除 Bootstrap 外） 正式回答\n**类加载器（ClassLoader）**是 JVM 的一个核心组件，负责将 .class 字节码文件加载到内存中，并生成对应的 Class 对象，使得程序能使用该类。\nJava 内置了以下几种类加载器：\nBootstrapClassLoader（启动类加载器）：由 C++ 实现，是 JVM 自身的一部分。负责加载 JDK 核心类库，如 rt.jar、java.lang、java.util 等。开发者无法直接获取该加载器的引用（getParent() 返回 null）。 ExtClassLoader（扩展类加载器）：负责加载 JDK 扩展目录 jre/lib/ext 中的类（Java 9 之后改为模块系统 PlatformClassLoader）。 AppClassLoader（应用类加载器）：负责加载 classpath 下的类，是程序默认使用的类加载器，加载开发者自己编写的类和第三方 jar。 自定义类加载器：通过继承 ClassLoader 实现，常用于 Tomcat 热加载、模块隔离、加密解密等场景。 这些类加载器之间形成了父子关系（通过 getParent()），但不是继承关系，而是组合（parent 字段）。\n什么是双亲委派模型？ 回答大纲\n加载类时，优先委派给父加载器尝试加载 父加载器无法加载时，才由自己尝试加载 体现了\u0026quot;先向上，再向下\u0026quot;的加载链 通过 loadClass 中的 parent.loadClass() 实现 正式回答\n**双亲委派模型（Parents Delegation Model）**是 JVM 默认的类加载行为机制，它的核心原则是：\n当一个类加载器收到类加载请求时，它不会自己先尝试加载，而是先委派给父加载器去尝试加载；每一层都是如此；只有当所有父加载器都无法加载该类时（在自己的搜索范围内没有找到对应的 .class），子加载器才尝试自己加载。\n源码体现在 ClassLoader.loadClass()：\nprotected Class\u0026lt;?\u0026gt; loadClass(String name, boolean resolve) { // 1. 检查是否已被加载 Class\u0026lt;?\u0026gt; c = findLoadedClass(name); if (c == null \u0026amp;\u0026amp; parent != null) { // 2. 委派给父加载器 c = parent.loadClass(name, false); } if (c == null) { // 3. 父加载器无法加载时才自己 findClass c = findClass(name); } return c; } 例如一个 AppClassLoader 收到加载 com.example.User 的请求：\nAppClassLoader 委派给 ExtClassLoader； ExtClassLoader 委派给 BootstrapClassLoader； BootstrapClassLoader 在 rt.jar 中找不到，进入下一层； ExtClassLoader 在 ext 目录中也找不到，进入下一层； AppClassLoader 在 classpath 下找到并加载。 为什么使用双亲委派机制？ 回答大纲\n类的唯一性：保证同一个类在 JVM 中只被一个加载器加载一次 安全性：防止核心 API 库被篡改 用户自定义 java.lang.String 不会被加载 防止通过自定义类加载器植入恶意代码 正式回答\n双亲委派机制的存在有两个核心目的：\n保证类的唯一性：同一份 .class 文件无论在 JVM 中哪个位置被引用，最终都会被同一个类加载器加载，避免出现\u0026quot;同一个类有多个 Class 对象\u0026quot;导致的类型混乱（ClassNotFoundException、类型比较失败等）。这种唯一性是 JVM 安全沙箱的基础。 保证 JDK 核心 API 的安全性：通过双亲委派，核心 API（如 java.lang.String、java.util.HashMap）只会被 BootstrapClassLoader 加载一次。攻击者即便写了一个 java.lang.String 类，由于加载请求会先到 BootstrapClassLoader，最终加载的是 JDK 自带的 String，用户写的同名类根本没有机会被执行。这从根本上防止了核心 API 被恶意篡改的可能性。 Spring 等框架为了实现某些高级功能（如 Tomcat 的 Web 应用隔离、OSGi 模块化、热部署），会破坏双亲委派模型，采用\u0026quot;线程上下文类加载器（Thread Context ClassLoader）\u0026ldquo;或自定义类加载器来实现。\n类装载的过程？ 回答大纲\n三大阶段 加载（Loading） 连接（Linking）：验证、准备、解析 初始化（Initialization） 准备阶段：static 变量分配内存 + 设默认初始值 解析阶段：符号引用 → 直接引用 正式回答\n类装载的全过程分为加载、连接、初始化三大阶段，其中连接又分为验证、准备、解析。\n加载：\n通过类的全限定名获取 .class 字节流（不限定来源，文件、网络、动态生成都行）。 把字节流转化为方法区的运行时数据结构。 在堆中生成一个代表该类的 Class 对象，作为方法区数据的访问入口。 连接：\n验证（Verify）：校验 .class 文件格式、字节码语义、引用合法性等，确保不会危害 JVM 安全。可以通过 -Xverify:none 关闭（不推荐）。 准备（Prepare）：为类变量（static 变量）分配内存并设置默认初始值。需要注意的是： static final 常量在准备阶段就会完成赋值（因为 final 字段在编译期就内联了）。 普通 static 变量准备阶段赋的是零值（如 int=0、boolean=false），赋值要到初始化阶段。 引用类型的 static 变量，准备阶段仅分配内存并赋 null，初始化阶段赋值。 解析（Resolve）：将符号引用转换为直接引用。 符号引用：java/lang/System.out.println() 这种基于方法名/字段名的字符串引用。 直接引用：直接指向目标的指针、句柄或偏移量，能在内存中定位到目标。 初始化：执行类的 \u0026lt;clinit\u0026gt; 方法（类构造器），按照源码中的顺序对类变量和静态语句块进行真正的赋值操作。这是类装载过程的最后一步，多个线程并发初始化同一个类时，JVM 会加锁保证只执行一次。\n静态代码块存在的意义 回答大纲\n典型场景 加载本地库：JNI 加载 .so / .dll 读取配置文件：把配置加载到内存 注册驱动：JDBC DriverManager 注册驱动 单例模式：Holder 模式按需创建单例 预置缓存：把高频访问的数据提前加载 正式回答\n静态代码块（static initializer）是伴随类初始化阶段自动执行的代码块，常用于执行类级别的\u0026rdquo;一次性\u0026ldquo;准备工作。它的核心价值在于：保证这些初始化操作有且仅有一次，且发生在类被第一次使用时（线程安全）。典型应用场景包括：\n加载本地库：JNI 调用前用 System.loadLibrary(\u0026quot;xxx\u0026quot;) 加载 .so 或 .dll 文件。 读取配置文件：将 application.yml、config.properties 等加载到内存中的某个工具类。 注册驱动：JDBC 中 Class.forName(\u0026quot;com.mysql.jdbc.Driver\u0026quot;) 会触发 DriverManager.registerDriver，新版 JDBC 也支持 SPI 自动注册，仍有类似静态初始化机制。 单例模式：借助 static 内部类（Holder 模式）实现线程安全的懒加载单例，例如 private static class Holder { static final Singleton INSTANCE = new Singleton(); }，外部访问 Holder.INSTANCE 触发内部类的初始化。 预置缓存：把字典项、枚举映射、热门数据等预置到静态集合中，避免每次请求都重新构造。 注意：静态代码块只在类首次主动使用时执行，且只执行一次；如果出现异常会导致类初始化失败，后续访问该类时抛 ExceptionInInitializerError。\nJVM 垃圾回收 对象什么时候可以被垃圾回收器回收？ 回答大纲\n核心判断原则：对象没有任何引用指向它时，可以被回收 两种主流判定算法 引用计数法：维护引用计数 可达性分析算法：从 GC Roots 出发搜索引用链 判定为可回收的时机：成为\u0026quot;不可达\u0026quot;对象后并非立即被回收，要经过两次标记 finalize() 方法只能\u0026quot;自救\u0026quot;一次 正式回答\n当一个对象没有任何引用指向它时，它就可以被垃圾回收器回收（前提是已经经过了一次可达性分析判定为不可达）。判断一个对象是否可回收主要有两种算法：\n引用计数法：对象维护一个引用计数器，每次被引用就 +1，引用失效就 -1。当计数器为 0 时，判定为可回收。\n优点：简单高效，判定过程可以穿插在程序运行中。 致命缺点：无法解决循环引用问题。例如对象 A 持有 B 的引用，B 也持有 A 的引用，二者计数器永不为 0，永远不会被回收。因此主流 JVM 都不采用该算法。 可达性分析算法（Tracing）：从一系列称为 GC Roots 的根对象出发，向下搜索所有走得到的引用链，能走到的对象是\u0026quot;可达的\u0026rdquo;，否则是\u0026quot;不可达的\u0026quot;。\nGC Roots 主要包括：虚拟机栈中的局部变量、native 方法引用的对象、方法区中的静态字段 / 常量、被同步锁持有的对象、虚拟机内部的引用（如 Class 对象）等。 是 HotSpot、JVM 等主流虚拟机采用的判定算法。 即便被判定为不可达，对象也不会被立即回收。它会先被标记并放进一个\u0026quot;F-Queue\u0026quot;队列，由一条低优先级的 Finalizer 线程去执行 finalize() 方法（最后一次自救的机会），如果 finalize 中重新建立了到 GC Roots 的链，则对象\u0026quot;复活\u0026quot;。但 Finalizer 线程并发性能差且行为不可预测，Java 9 起已被标记为 deprecated。\nJVM 垃圾回收的算法有哪些？ 回答大纲\n标记清除算法 标记整理算法 复制算法 正式回答\nJVM 的垃圾回收算法主要有三种。\n标记清除算法（Mark-Sweep）将垃圾回收分为\u0026quot;标记\u0026quot;和\u0026quot;清除\u0026quot;两个阶段：首先标记出所有需要回收的对象，标记完成后统一回收所有被标记的对象。这种方法效率较高，但由于不再使用的对象被回收后，内存中会产生大量不连续的内存碎片，导致后续申请大对象时无法找到足够的连续内存而不得不触发另一次 GC。 标记整理算法（Mark-Compact）在标记清除算法的基础上做了改进。标记阶段相同，但在清除阶段不是直接清除垃圾对象，而是把存活的对象都向内存的一端移动，然后直接清理边界以外的内存。这种方式没有内存碎片，但因为需要移动对象，所以效率较低，且移动对象时需要暂停所有用户线程（STW）。 复制算法（Copying）把可用的内存按容量划分为大小相等的两块，每次只使用其中一块。当这一块的内存用完时，把还存活的对象复制到另一块内存中，再把已使用的那块内存全部清空，并交换两块内存的角色。这种方式没有内存碎片，但因为始终只有一块内存可用，所以内存利用率只有 50%。 现代 JVM 通常结合使用多种算法（即分代收集理论）：新生代使用复制算法，老年代使用标记清除或标记整理算法，以扬长避短。\n什么是分代回收？各代的回收策略？ 回答大纲\n分代依据：大部分对象生命周期短 新生代：复制算法（Eden + Survivor） 老年代：标记整理 / 标记清除 Minor GC / Major GC / Full GC 对象晋升：年龄阈值、动态年龄判断、大对象直接进老年代 正式回答\n分代回收（Generational Collection）是当前商业虚拟机普遍采用的垃圾回收策略，其核心思想是基于 \u0026ldquo;弱分代假说\u0026rdquo;：绝大多数对象都是\u0026quot;朝生夕死\u0026quot;的，而熬过越多次 GC 的对象就越难消亡。基于此，JVM 将堆划分为新生代和老年代两块，分别采用不同的回收算法。\n新生代（Young Gen）： 存放新创建的对象。 划分为 Eden 区（80%）+ Survivor To + Survivor From（各 10%）。 采用复制算法：Minor GC 时，Eden + From 中存活的对象被复制到 To 区，然后 From 和 To 互换角色。 触发频率高，但单次 STW 时间短。 老年代（Old Gen）： 存放长期存活的对象。 采用标记清除或标记整理算法。 触发频率低，但单次 STW 时间通常更长，Full GC 会导致应用明显卡顿。 晋升机制： 对象每熬过一次 Minor GC，年龄 +1，默认 15 岁时晋升老年代（-XX:MaxTenuringThreshold）。 Survivor 区中同龄对象总大小超过 Survivor 50% 时，比该年龄大的对象直接晋升（动态年龄判断）。 大对象（如长字符串、大数组）会直接进入老年代，避免在 Eden 和 Survivor 间来回复制。 回收触发： Minor GC：Eden 区满时触发，仅回收新生代。 Major GC / Old GC：老年代空间不足时触发，仅回收老年代。 Full GC：回收整个堆 + 方法区，会暂停所有用户线程，应尽量避免。 跨代引用：通过 Card Table（卡表） + Remembered Set 解决，避免扫描整个老年代。 什么是 GC Roots？ 回答大纲\n可达性分析的起点 常见 GC Roots 虚拟机栈中的局部变量 静态字段引用的对象 常量池中的引用 native 方法引用的对象 synchronized 持有的对象 JVM 内部引用（如基本类型的 Class 对象、异常对象） 正式回答\nGC Roots 是可达性分析算法中作为搜索起点的对象集合。从这些起点出发向下搜索，搜索经过的链路称为\u0026quot;引用链\u0026quot;，当一个对象到 GC Roots 没有任何引用链相连时（即不可达），就判定为可被回收。\n常见的 GC Roots 包括：\n**虚拟机栈（栈帧中的局部变量表）**中引用的对象。例如方法参数、局部变量。 方法区中静态属性引用的对象。例如 static User user = new User() 中的 user。 方法区中常量引用的对象。例如 \u0026quot;abc\u0026quot; 这种字符串常量池中的引用。 本地方法栈（JNI）中 native 方法引用的对象。 被同步锁（synchronized）持有的对象。 JVM 内部引用：如基本数据类型的 Class 对象、异常对象（NullPointerException、OutOfMemoryError）、系统类加载器、预分配的内存对象等。 其他：如被加入到 \u0026ldquo;GC Roots Set\u0026rdquo; 的引用，记录在 CardTable / RSet 中的来自老年代的跨代引用。 判断一个对象能否被回收，只看它是否能从这些根被搜索到，跟其他业务字段无关。这也是 Java 内存泄漏的常见根源之一：通过静态集合意外持有了不再需要的对象，使对象无法被回收。\n分代回收 MinorGC , MixedGC , FullGC 的区别 JVM 有哪些垃圾回收器 串行 新生代用 Serial（复制算法），老年代用 Serial Old（标记-整理算法）。 并行（JDK8） 新生代用 Parallel Scavenge（复制算法，多线程），老年代用 Parallel Old（标记-整理算法，多线程）。 CMS（只负责老年代） G1（JDK9+默认） 聊一聊G1垃圾回收器 强引用、软引用、弱引用、虚引用的区别？ JVM调优参数可以在哪里设置参数值 war包 - tomcat jar包 - 启动参数 JVM调优参数有哪些？ 说一说JVM调优的工具？ java内存泄漏的排查思路？ 获取内存快照 jmap获取dump文件 vm参数获取dump文件 VisualVM 分析 dump文件 查看堆信息情况 找到对应代码进行分析 CPU飙高的排查方案与思路 top ps H -eo pid, tid, %cpu | grep 40940 jstack 40940 printf \u0026ldquo;%x\\n\u0026rdquo; 40955 设计模式 工厂设计模式 简单工厂模式 具体产品 具体工厂 抽象工厂模式 策略模式 策略+工厂 只要有if-else，冗长的switch分支，都可以使用策略模式 责任链设计模式 单点登录这块是如何实现的 权限认证是如何实现的 RBAC 上传数据的安全性你们是怎么控制的 对称加密 非对称加密 负责项目时遇到了哪些比较棘手的问题，如何解决的 设计模式 工厂 策略 责任链 线上BUG CPU飙高 内存泄漏 线程死锁 调优 接口慢 慢SQL 缓存方案 组件封装 分布式锁 接口幂等 分布式事务 支付通用 你们项目中日志是怎么采集的？ 采集日志的手段 ELK ElasticSearch Logstash Kibana 常规采集 查看日志的命令？ LINUX\ntail -f xx.log\ntail -n 100 xx.log\nhead -n 100 xx.log\ncat -n xx.log | tail -n + 100 | head -n 100\ncat -n xx.log | grep \u0026ldquo;debug\u0026rdquo;\n按日期查询 sed -n \u0026hellip;\n生产环境下的问题怎么排查 分析日志 远程debug（生产不允许debug） 远程代码和本地代码保持一致 怎么快速定位系统的瓶颈 压测 监控工具 Prometheus + Grafana Skywalking, Zipkin 线上诊断工具 Arthas ","permalink":"https://lv-blog.pages.dev/posts/programming/for-interview/java-interview-questions/","summary":"\u003ch1 id=\"集合\"\u003e集合\u003c/h1\u003e\n\u003ch2 id=\"为什么数组索引要从-0-开始从-1-开始不行吗\"\u003e为什么数组索引要从 0 开始？从 1 开始不行吗？\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e回答大纲\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e数组在内存中是连续存储的，CPU 通过寻址公式访问元素\u003c/li\u003e\n\u003cli\u003e从 0 开始：\u003ccode\u003ea[i]_address = base_address + i * typeSize\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e从 1 开始：\u003ccode\u003ebase_address + (i-1) * typeSize\u003c/code\u003e，多一次减法\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e正式回答\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e数组在内存中是连续存储的，CPU 通过寻址公式来访问指定下标的元素：\u003ccode\u003ea[i] 的地址 = 数组首地址 + i * 元素大小\u003c/code\u003e。当数组下标从 0 开始时，直接套用上述公式即可计算偏移量；如果下标从 1 开始，则需要先执行 \u003ccode\u003ei - 1\u003c/code\u003e 再做乘法，多了一次减法运算。虽然单次访问的差异可以忽略不计，但数组通常会出现在循环、遍历等高频访问的场景中，这笔额外开销会被无限放大。这也是 C 语言沿用下来的历史约定，Java 作为一门追求高效的系统级语言，继承了这一设计。\u003c/p\u003e\n\u003ch2 id=\"时间复杂度如何计算的\"\u003e时间复杂度如何计算的？\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e回答大纲\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e口诀：常 对 幂 指 阶\u003c/li\u003e\n\u003cli\u003e常见复杂度\n\u003cul\u003e\n\u003cli\u003e常数复杂度 O(1)：不随 n 变化\u003c/li\u003e\n\u003cli\u003e对数复杂度 O(logN)：二分查找、树操作\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e正式回答\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e时间复杂度用于衡量算法执行时间随数据规模 n 增长的变化趋势，是大 O 表示法下对最坏情况的抽象描述。常见的大小关系可用口诀\u0026quot;常对幂指阶\u0026quot;来记忆，即 O(1) \u0026lt; O(logN) \u0026lt; O(N) \u0026lt; O(N*logN) \u0026lt; O(N²) \u0026lt; O(2ⁿ) \u0026lt; O(n!)，越靠后算法越慢。\u003c/p\u003e","title":"Java 面试题整理"},{"content":"2024 到 2025 年这两年，阿里云、腾讯云、网易、科大讯飞，还有各大高校的公共 Docker 镜像站，基本上是一波接一波地关的关、限的限。到了 2026 年，\u0026ldquo;拉个镜像如渡劫\u0026quot;这句话已经不是玩笑了。\n很多人第一反应是骂大厂抠门。但说实话，这事没那么简单。\n合规这座山绕不过去 Docker Hub 是个全球开放的社区，任何人都能往上面推镜像，没有人在入口做内容审查。这就意味着里面什么都有——有漏洞的镜像、带恶意软件的镜像，乃至触碰国内监管红线的内容，全都混在里面。\n问题在于，只要阿里云的服务器把这些东西同步下来，再转发给用户，阿里云就天然地成了\u0026quot;中间人\u0026rdquo;。在现行的监管逻辑下，平台不能说\u0026quot;我只是个镜子，内容不是我的\u0026quot;——你提供了传播通道，你就得负责。这种连带风险，没有哪家大厂愿意为一个免费服务去扛。\n带宽这笔账算不过来 Docker 镜像这个东西和 npm、pip 那些包依赖有本质的不同——它就是重。一个普通的 Node.js 基础镜像几百 MB 起步，稍微带点 CUDA 或者 PyTorch 环境的，动辄就是好几个 GB。\n国内用镜像源的群体又特别杂：开发者、CI/CD 流水线、还有成千上万台家用 NAS——群晖、极空间这些，很多人设置成定时自动更新，24 小时都在跑。这些流量全是公网出口流量，大厂一分钱都收不到，还得自己掏带宽的钱。\n原本这种事情算是一种社区口碑投资，但现在各家都在降本增效，这种\u0026quot;纯出血、零转化\u0026quot;的项目，砍起来是毫不手软的。\n大厂没有放弃加速，只是换了玩法 阿里云和腾讯云并没有把镜像加速这个能力整个下线，而是把它圈起来了。\n现在的逻辑大概是这样的：你如果买了他们的 ECS，在内网环境里拉镜像，依然是快的、免费的。你如果想在公网用，那你得注册账号、开通 ACR（容器镜像服务），生成一个绑定了你身份的专属加速地址。一旦你的账号有异常，随时可以溯源和封禁。\n换句话说，大厂的态度变成了：你是我的付费用户，或者至少是实名用户，我可以给你开小灶；你要是想匿名白嫖，那就没有这个服务了。\n那现在大家怎么办？ 目前社区里摸索出来的路子主要有几条。\n一种是去找垂直的合规镜像站，比如开放原子基金会的 AtomHub，但它只收录了三百多个经过安全审计的基础镜像，能找到自己想要的东西的概率不高。还有一些商业和公益混合性质的服务，比如轩辕镜像、毫秒镜像，口碑还行，但也说不准能撑多久。\n另一种是去开通阿里云或腾讯云的个人 ACR，拿到专属链接，老老实实实名用。\n最彻底的做法，是自己在香港或者新加坡搞一台便宜的 VPS，用 Nginx 或者 Cloudflare Workers 自建一个反向代理，专供自己用，不对外开放。速度有保障，也不怕被人蹭流量拖垮。很多企业内部和有折腾精神的开发者，现在基本都走这条路了。\n","permalink":"https://lv-blog.pages.dev/posts/programming/why-why-why/why-rare-docker-images/","summary":"\u003cp\u003e2024 到 2025 年这两年，阿里云、腾讯云、网易、科大讯飞，还有各大高校的公共 Docker 镜像站，基本上是一波接一波地关的关、限的限。到了 2026 年，\u0026ldquo;拉个镜像如渡劫\u0026quot;这句话已经不是玩笑了。\u003c/p\u003e\n\u003cp\u003e很多人第一反应是骂大厂抠门。但说实话，这事没那么简单。\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"合规这座山绕不过去\"\u003e合规这座山绕不过去\u003c/h3\u003e\n\u003cp\u003eDocker Hub 是个全球开放的社区，任何人都能往上面推镜像，没有人在入口做内容审查。这就意味着里面什么都有——有漏洞的镜像、带恶意软件的镜像，乃至触碰国内监管红线的内容，全都混在里面。\u003c/p\u003e\n\u003cp\u003e问题在于，只要阿里云的服务器把这些东西同步下来，再转发给用户，阿里云就天然地成了\u0026quot;中间人\u0026rdquo;。在现行的监管逻辑下，平台不能说\u0026quot;我只是个镜子，内容不是我的\u0026quot;——你提供了传播通道，你就得负责。这种连带风险，没有哪家大厂愿意为一个免费服务去扛。\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"带宽这笔账算不过来\"\u003e带宽这笔账算不过来\u003c/h3\u003e\n\u003cp\u003eDocker 镜像这个东西和 npm、pip 那些包依赖有本质的不同——它就是重。一个普通的 Node.js 基础镜像几百 MB 起步，稍微带点 CUDA 或者 PyTorch 环境的，动辄就是好几个 GB。\u003c/p\u003e\n\u003cp\u003e国内用镜像源的群体又特别杂：开发者、CI/CD 流水线、还有成千上万台家用 NAS——群晖、极空间这些，很多人设置成定时自动更新，24 小时都在跑。这些流量全是公网出口流量，大厂一分钱都收不到，还得自己掏带宽的钱。\u003c/p\u003e\n\u003cp\u003e原本这种事情算是一种社区口碑投资，但现在各家都在降本增效，这种\u0026quot;纯出血、零转化\u0026quot;的项目，砍起来是毫不手软的。\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"大厂没有放弃加速只是换了玩法\"\u003e大厂没有放弃加速，只是换了玩法\u003c/h3\u003e\n\u003cp\u003e阿里云和腾讯云并没有把镜像加速这个能力整个下线，而是把它圈起来了。\u003c/p\u003e\n\u003cp\u003e现在的逻辑大概是这样的：你如果买了他们的 ECS，在内网环境里拉镜像，依然是快的、免费的。你如果想在公网用，那你得注册账号、开通 ACR（容器镜像服务），生成一个绑定了你身份的专属加速地址。一旦你的账号有异常，随时可以溯源和封禁。\u003c/p\u003e\n\u003cp\u003e换句话说，大厂的态度变成了：你是我的付费用户，或者至少是实名用户，我可以给你开小灶；你要是想匿名白嫖，那就没有这个服务了。\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"那现在大家怎么办\"\u003e那现在大家怎么办？\u003c/h3\u003e\n\u003cp\u003e目前社区里摸索出来的路子主要有几条。\u003c/p\u003e\n\u003cp\u003e一种是去找垂直的合规镜像站，比如开放原子基金会的 AtomHub，但它只收录了三百多个经过安全审计的基础镜像，能找到自己想要的东西的概率不高。还有一些商业和公益混合性质的服务，比如轩辕镜像、毫秒镜像，口碑还行，但也说不准能撑多久。\u003c/p\u003e\n\u003cp\u003e另一种是去开通阿里云或腾讯云的个人 ACR，拿到专属链接，老老实实实名用。\u003c/p\u003e\n\u003cp\u003e最彻底的做法，是自己在香港或者新加坡搞一台便宜的 VPS，用 Nginx 或者 Cloudflare Workers 自建一个反向代理，专供自己用，不对外开放。速度有保障，也不怕被人蹭流量拖垮。很多企业内部和有折腾精神的开发者，现在基本都走这条路了。\u003c/p\u003e","title":"为什么国内大厂全面撤掉了公共 Docker 镜像源？"},{"content":"","permalink":"https://lv-blog.pages.dev/posts/programming/backend/cloudflare/","summary":"","title":"Cloudflare 业务学习"},{"content":" 面试题从互联网各个角落收集而来\n谈一谈 Spring IOC 的底层实现？ 反射 工厂的价值 设计模式 关键的几个方法 createBeanFactory getBean doGetBean createBean doCreateBean createBeanInstance(getDeclaredConstructor, newInstance)「方案选单」 populateBean flowchart TD A[开始] --\u003e B[\"createBeanFactory()创建 DefaultListableBeanFactory\"] B --\u003e C[加载 BeanDefinition解析 XML/注解] C --\u003e D[\"getBean(beanName)外部调用入口\"] D --\u003e E[\"doGetBean(beanName)\"] E --\u003e F{从缓存获取单例?} F --\u003e|是| G[返回缓存的 Bean] F --\u003e|否| H[\"createBean(beanName, mbd)\"] H --\u003e I[\"doCreateBean(beanName, mbd)\"] I --\u003e J[\"createBeanInstance()getDeclaredConstructor + newInstance\"] J --\u003e K[\"populateBean()属性填充/依赖注入\"] K --\u003e L[\"initializeBean()初始化回调\"] L --\u003e M[注册销毁方法] M --\u003e N[返回完整 Bean 实例] 谈谈 SpringIOC 的理解，原理和实现？ IOC 思想 DI 实现手段 什么是容器 什么是Bean BeanDefination 从哪里读 XML 注解 存哪里 所有 BeanDefinition 都存在 DefaultListableBeanFactory 里的一个 Map 中 容器的生命周期 flowchart TD START([🚀 程序启动]) --\u003e REFRESH[\"AbstractApplicationContext.refresh()\"] REFRESH --\u003e PHASE1 subgraph PHASE1[\"🔵 第一阶段：准备工作\"] direction TB A1[\"prepareRefresh()\\n记录启动时间\\n设置容器状态为「活跃」\\n初始化环境变量\"] --\u003e A2 A2[\"obtainFreshBeanFactory()\\n创建 DefaultListableBeanFactory\\n这是容器的核心仓库\"] --\u003e A3 A3[\"prepareBeanFactory()\\n注册基础 BeanPostProcessor\\n注册 Aware 相关处理器\\n注册默认环境 Bean（environment）\"] end PHASE1 --\u003e PHASE2 subgraph PHASE2[\"🟢 第二阶段：加载 BeanDefinition（读图纸）\"] direction TB B1[\"扫描 @ComponentScan 指定的包\\n或读取 XML 配置文件\"] --\u003e B2 B2[\"解析注解 / XML\\n识别 @Component @Service\\n@Repository @Controller @Bean\"] --\u003e B3 B3[\"为每个 Bean 生成 BeanDefinition\\n记录：类名、作用域、是否懒加载\\n初始化方法、销毁方法、依赖关系\"] --\u003e B4 B4[(\"存入 beanDefinitionMap\\nkey = beanName\\nvalue = BeanDefinition\")] end PHASE2 --\u003e PHASE3 subgraph PHASE3[\"🟡 第三阶段：修改 BeanDefinition（修图纸）\"] direction TB C1[\"invokeBeanFactoryPostProcessors()\\n执行所有 BeanFactoryPostProcessor\"] --\u003e C2 C2[\"ConfigurationClassPostProcessor\\n处理 @Configuration @Import\\n@PropertySource @ComponentScan\\n👉 SpringBoot 自动装配在此触发\"] --\u003e C3 C3[\"PropertySourcesPlaceholderConfigurer\\n替换 BeanDefinition 中\\n所有 ${xxx} 占位符为真实值\"] --\u003e C4 C4[/\"所有 BeanDefinition 最终确定\\n图纸不再变动\"/] end PHASE3 --\u003e PHASE4 subgraph PHASE4[\"🌸 第四阶段：注册 BeanPostProcessor（工人就位）\"] direction TB D1[\"registerBeanPostProcessors()\\n按优先级顺序注册\\nPriorityOrdered → Ordered → 普通\"] --\u003e D2 D2[\"AutowiredAnnotationBeanPostProcessor\\n负责处理 @Autowired @Value\"] --\u003e D3 D3[\"CommonAnnotationBeanPostProcessor\\n负责处理 @PostConstruct @PreDestroy @Resource\"] --\u003e D4 D4[\"AnnotationAwareAspectJAutoProxyCreator\\n负责检测切面、生成 AOP 代理对象\"] --\u003e D5 D5[/\"所有工人就位\\n等待 Bean 创建时介入\\n此时不开工\"/] end PHASE4 --\u003e PHASE5 subgraph PHASE5[\"🟣 第五阶段：容器基础设施初始化\"] direction TB E1[\"initMessageSource()\\n初始化国际化资源 i18n\"] --\u003e E2 E2[\"initApplicationEventMulticaster()\\n初始化事件广播器\"] --\u003e E3 E3[\"onRefresh()\\n⭐ SpringBoot 在此启动\\nTomcat / Jetty / Undertow\\nWeb 容器开始监听端口\"] --\u003e E4 E4[\"registerListeners()\\n注册所有 ApplicationListener\\n监听容器事件\"] end PHASE5 --\u003e PHASE6 subgraph PHASE6[\"🟠 第六阶段：实例化所有单例 Bean\"] direction TB F1[\"finishBeanFactoryInitialization()\\npreInstantiateSingletons()\\n遍历所有非懒加载单例 BeanDefinition\"] --\u003e F2 F2[\"逐个执行 getBean()\\n触发每个 Bean 的生命周期\\n实例化 → 属性填充 → Aware回调\\n→ 前置处理 → init方法 → 后置处理\"] --\u003e F3 F3[(\"所有单例 Bean 存入\\nsingletonObjects 一级缓存\\n✅ 全部就绪\")] end PHASE6 --\u003e PHASE7 subgraph PHASE7[\"✅ 第七阶段：容器就绪\"] direction TB G1[\"finishRefresh()\\n清理启动时占用的资源\\n初始化生命周期处理器\"] --\u003e G2 G2[\"发布 ContextRefreshedEvent\\n通知所有监听者：容器启动完成\"] --\u003e G3 G3[/\"容器进入运行状态\\n可以处理业务请求\"/] end G3 --\u003e RUNNING([🎉 容器正常运行中]) RUNNING -.-\u003e|\"收到关闭信号\\nCtrl+C / kill / close()\"| PHASE8 subgraph PHASE8[\"🔴 第八阶段：容器销毁\"] direction TB H1[\"发布 ContextClosedEvent\\n通知所有监听者：容器即将关闭\"] --\u003e H2 H2[\"停止 Web 容器\\nTomcat 停止接受新请求\"] --\u003e H3 H3[\"执行所有 Bean 的销毁逻辑\\n@PreDestroy → destroy() → destroy-method\\n按注册顺序逆序销毁\"] --\u003e H4 H4[\"清空所有缓存\\nsingletonObjects 清空\\nbeanDefinitionMap 清空\"] --\u003e H5 H5[\"容器状态设置为「关闭」\\nactive = false / closed = true\"] end H5 --\u003e END([💀 容器销毁完成]) style START fill:#43A047,color:#fff style RUNNING fill:#43A047,color:#fff style END fill:#e53935,color:#fff style REFRESH fill:#1E88E5,color:#fff bean 的生命周期 flowchart TD START([🌱 开始创建 Bean]) --\u003e A A[\"① 实例化\\nInstantiation\\n反射调用构造方法\\nnew 出空壳对象\\n此时所有字段都是 null\"] --\u003e B B[\"② 放入三级缓存\\nsingletonFactories\\n提前暴露半成品\\n为循环依赖做准备\"] --\u003e C C[\"③ 属性填充\\npopulateBean()\\n处理 @Autowired @Value\\n把依赖的 Bean 注入进来\"] --\u003e D subgraph AWARE[\"④ Aware 回调\"] direction TB D[\"BeanNameAware\\n告诉 Bean 自己叫什么名字\"] --\u003e E E[\"BeanFactoryAware\\n把 BeanFactory 塞给 Bean\"] --\u003e F F[\"ApplicationContextAware\\n把 ApplicationContext 塞给 Bean\"] end subgraph INIT[\"⑤ 初始化 initializeBean()\"] direction TB G[\"前置处理\\npostProcessBeforeInitialization()\\n所有 BeanPostProcessor 挨个执行\\n📌 @PostConstruct 在这里被调用\"] --\u003e INITMETHOD subgraph INITMETHOD[\"init 方法（按顺序执行）\"] direction TB H1[\"第1个：@PostConstruct\\n（已在前置处理中执行）\"] --\u003e H2 H2[\"第2个：afterPropertiesSet()\\n实现 InitializingBean 接口\"] --\u003e H3 H3[\"第3个：init-method\\n@Bean(initMethod='xxx') 或 XML 配置\"] end INITMETHOD --\u003e I I[\"后置处理\\npostProcessAfterInitialization()\\n所有 BeanPostProcessor 挨个执行\\n⭐ AOP 代理在这里生成\"] end AWARE --\u003e INIT I --\u003e J subgraph CACHE[\"⑥ 存入缓存\"] direction TB J[\"从三级缓存 singletonFactories 移除\\n从二级缓存 earlySingletonObjects 移除\"] --\u003e K K[\"存入一级缓存 singletonObjects\\n✅ 完整 Bean 就绪\"] end K --\u003e READY([🎉 Bean 可以正常使用了]) READY -.-\u003e|\"容器关闭\"| DESTROY subgraph DESTROY[\"⑦ 销毁阶段（容器关闭时）\"] direction TB D1[\"第1个：@PreDestroy\\n方法被调用\"] --\u003e D2 D2[\"第2个：destroy()\\n实现 DisposableBean 接口\"] --\u003e D3 D3[\"第3个：destroy-method\\n@Bean(destroyMethod='xxx') 或 XML 配置\"] end D3 --\u003e END([💀 Bean 销毁完成]) spring 三级缓存依赖流程 sequenceDiagram participant Spring as Spring容器 participant L3 as 三级缓存singletonFactories participant L2 as 二级缓存earlySingletonObjects participant L1 as 一级缓存singletonObjects Note over Spring: 开始创建 A Spring-\u003e\u003eSpring: ① new A 空壳对象 Spring-\u003e\u003eL3: ② 存入 A 的 ObjectFactory lambda Note over Spring: 给 A 注入属性，发现需要 B Spring-\u003e\u003eSpring: ③ new B 空壳对象 Spring-\u003e\u003eL3: ④ 存入 B 的 ObjectFactory lambda Note over Spring: 给 B 注入属性，发现需要 A Spring-\u003e\u003eL1: ⑤ getBean(A)，查一级缓存 L1--\u003e\u003eSpring: ❌ 没有 Spring-\u003e\u003eL2: 查二级缓存 L2--\u003e\u003eSpring: ❌ 没有 Spring-\u003e\u003eL3: 查三级缓存 L3--\u003e\u003eSpring: ✅ 找到 A 的 lambda！ Spring-\u003e\u003eSpring: ⑥ 执行 lambda（需要代理则生成代理 A） Spring-\u003e\u003eL2: ⑦ 早期引用放入二级缓存 Spring-\u003e\u003eL3: 删除三级缓存中的 A Note over Spring: B 拿到 A 的早期引用，B 完成初始化 Spring-\u003e\u003eL1: ⑧ B 放入一级缓存 ✅ Spring-\u003e\u003eL3: 删除三级缓存中的 B Note over Spring: 回到 A 的流程，B 已就绪，A 完成初始化 Spring-\u003e\u003eL1: ⑨ A 放入一级缓存 ✅ Spring-\u003e\u003eL2: 删除二级缓存中的 A Note over L1: 最终：一级缓存中有完整的 A 和 B ✅ 情况 三级缓存 lambda 执行 二级缓存 无循环依赖，无 AOP 存了 lambda ❌ 不执行 不经过 无循环依赖，有 AOP 存了 lambda ❌ 不执行 不经过 有循环依赖，无 AOP 存了 lambda ✅ 执行 存原始对象 有循环依赖，有 AOP 存了 lambda ✅ 执行 存代理对象 spring bean 缓存的放置时间和删除时间 Spring Bean 三级缓存的放置与删除时间 缓存 存的是什么 放入时机 删除时机 三级 singletonFactories Bean 的工厂 lambda 实例化完成后立刻放入 工厂被调用时（升级到二级）或 Bean 完成时 二级 earlySingletonObjects 早期暴露的半成品 Bean 三级工厂被调用的瞬间 Bean 完全初始化完成放入一级时 一级 singletonObjects 完整可用的 Bean 初始化全部完成后 容器关闭销毁时 BeanFactory 和 FactoryBean 的区别？ FactoryBean 是什么 public interface FactoryBean\u0026lt;T\u0026gt; { // 返回 Bean 的实例（可以是复杂创建逻辑） T getObject() throws Exception; // 返回 Bean 的类型 Class\u0026lt;?\u0026gt; getObjectType(); // 是否单例 default boolean isSingleton() { return true; } } 相同点 都是用来创建Bean对象的 不同点 使用 BeanFactory 创建对象的适合必须遵循严格的生命周期流程，太复杂了。如果想简单自定义某个对象的创建，同时想交给spring管理，那么必须实现 FactoryBean 接口 isSingleton 是否是单例对象 getObjectType 获取返回对象的类型 getObject 自定义创建对象的过程 对比维度 BeanFactory FactoryBean 角色 容器/工厂接口 特殊的 Bean 功能 管理所有 Bean 的生命周期 自定义某个 Bean 的创建逻辑 定位 基础设施（IOC 容器） 业务扩展（创建复杂对象） 使用方式 由 Spring 框架实现和使用 由开发者实现，注册到容器中 常见实现 DefaultListableBeanFactory SqlSessionFactoryBean、ProxyFactoryBean 谁创建谁 BeanFactory 创建并管理 FactoryBean FactoryBean 创建业务对象 flowchart LR subgraph BeanFactory[BeanFactory - IOC容器] direction LR Bean1[普通 Bean] Bean2[普通 Bean] FB[FactoryBean特殊 Bean] end FB --\u003e|调用 getObject| Result[业务对象] User[开发者] --\u003e|getBean| BF[BeanFactory] BF --\u003e|返回| Bean1 BF --\u003e|返回| Bean2 BF --\u003e|返回 getObject 结果| Result BF --\u003e|加 \u0026 前缀| FB Spring中用到的设计模式 单例模式 原型模式（指定作用域为prototype） 工厂模式 BeanFactory 模板方法 JdbcTemplate TransactionTemplate RestTemplate RedisTemplate 策略模式 XmlBeanDefinitionReader PropertiesBeanDefinitionReader 观察者模式 listener event multicast 适配器模式 HandlerAdapter 装饰者模式 BeanWrapper 责任链模式 使用aop的时候会先生成一个拦截器链 SpringMVC 的filter责任链 代理模式 动态代理 委托者模式 delegate Spring AOP 底层实现原理 🥇 第一层：一句话定性（开场） \u0026ldquo;Spring AOP 的底层本质是动态代理。Spring 在容器初始化 Bean 的时候，通过 BeanPostProcessor 机制拦截，判断这个 Bean 是否需要被增强，如果需要，就用动态代理生成一个代理对象，替换掉原始 Bean 注册进容器。后续所有对这个 Bean 的调用，实际上都是在走代理对象。\u0026rdquo;\n🥈 第二层：展开两种代理方式（核心） \u0026ldquo;具体的代理方式有两种——\u0026rdquo;\n\u0026ldquo;第一种是 JDK 动态代理，基于接口实现，用 Proxy.newProxyInstance 生成代理，目标类必须有接口。\u0026rdquo;\n\u0026ldquo;第二种是 CGLIB 动态代理，基于字节码在运行时生成目标类的子类，不需要接口，但目标类和方法不能是 final。\u0026rdquo;\n\u0026ldquo;Spring Boot 2.x 之后，默认强制走 CGLIB，除非手动配置 proxyTargetClass = false。\u0026rdquo;\n🥉 第三层：说清楚调用链路（亮点） \u0026ldquo;调用链路上，Spring 用 ReflectiveMethodInvocation 把所有匹配的 Advice 组装成一个拦截器链，通过递归 proceed() 依次执行。执行顺序是：@Around 前半段 → @Before → 目标方法 → @AfterReturning / @AfterThrowing → @After（finally）→ @Around 后半段。\u0026rdquo;\n💡 主动抛出一个坑，拉开差距 \u0026ldquo;这里有个常见的坑——同类内自调用会导致 AOP 失效。比如方法 A 内部用 this.B() 调用同类方法 B，走的是原始对象，绕过了代理，@Transactional 这种注解就不生效了。解决方案是注入自身的代理对象，或者用 AopContext.currentProxy() 拿到当前代理。\u0026rdquo;\n追问预案 面试官追问 你的应对方向 JDK 和 CGLIB 性能差异？ JDK 反射调用早期慢，JDK 8+ 有优化；CGLIB 创建慢但调用快；现代版本差距不大 Spring AOP 和 AspectJ 有什么区别？ Spring AOP 运行时代理，只能拦截 Spring 管理的 Bean 的方法；AspectJ 编译期/加载期织入字节码，功能更强（可拦截构造器、字段） @Transactional 为什么基于 AOP？ 它本质是一个 Around Advice，在方法前开启事务，正常结束提交，异常回滚 BeanPostProcessor 是什么时候触发的？ Bean 初始化完成后（initializeBean 最后阶段），postProcessAfterInitialization 被调用 为什么 final 方法不能被 CGLIB 代理？ CGLIB 是通过继承生成子类来覆盖方法，final 方法无法被子类覆盖，因此拦截不到 ❌ 常见回答误区 ❌ \u0026#34;Spring AOP 就是用了反射\u0026#34; → 太浅，反射只是 JDK 代理调用目标方法的手段 ❌ \u0026#34;CGLIB 比 JDK 代理快，所以 Spring 默认用 CGLIB\u0026#34; → 逻辑倒置，Spring Boot 2.x 改默认的原因主要是为了 避免「接口代理注入实现类」时的类型转换问题，不是纯性能考量 ❌ 把 AspectJ 的编译期织入当成 Spring AOP 的原理来说 → 混淆了两套体系，Spring AOP 默认不用 AspectJ 的织入器 SpringMVC 适配器请求处理流程 flowchart TD A([HTTP请求进来]) --\u003e B B[\"DispatcherServlet\\n.doDispatch()\"] B --\u003e C[\"HandlerMapping\\n.getHandler()\\n───────────\\n根据URL找到对应Handler\\n返回 HandlerExecutionChain\"] C --\u003e D{\"Handler是哪种类型？\"} D --\u003e|\"实现了Controller接口\\n（老式写法）\"| E1[\"SimpleControllerHandlerAdapter\\n.supports(handler) → true\"] D --\u003e|\"@RequestMapping注解方法\\n（现代写法）\"| E2[\"RequestMappingHandlerAdapter\\n.supports(handler) → true\"] D --\u003e|\"实现了HttpRequestHandler\\n（静态资源等）\"| E3[\"HttpRequestHandlerAdapter\\n.supports(handler) → true\"] E1 --\u003e F1[\"SimpleControllerHandlerAdapter\\n.handle()\\n───────────\\n内部强转后调用：\\n((Controller) handler)\\n.handleRequest(req, res)\"] E2 --\u003e F2[\"RequestMappingHandlerAdapter\\n.handle()\\n───────────\\n内部通过反射调用：\\nhandlerMethod.getMethod()\\n.invoke(bean, args)\"] E3 --\u003e F3[\"HttpRequestHandlerAdapter\\n.handle()\\n───────────\\n内部强转后调用：\\n((HttpRequestHandler) handler)\\n.handleRequest(req, res)\"] F1 --\u003e G[\"返回 ModelAndView\\n给 DispatcherServlet\"] F2 --\u003e G F3 --\u003e G G --\u003e H{\"有没有视图？\"} H --\u003e|\"有视图名\\n传统页面\"| I[\"ViewResolver\\n.resolveViewName()\\n───────────\\n解析成View对象\\n渲染HTML返回\"] H --\u003e|\"@ResponseBody\\nREST接口\"| J[\"MessageConverter\\n.write()\\n───────────\\n对象序列化为JSON\\n直接写入响应体\"] style B fill:#f4a261,color:#000 style E1 fill:#457b9d,color:#fff style E2 fill:#457b9d,color:#fff style E3 fill:#457b9d,color:#fff style F1 fill:#2a9d8f,color:#fff style F2 fill:#2a9d8f,color:#fff style F3 fill:#2a9d8f,color:#fff SpringMVC 请求处理完整流程 sequenceDiagram actor 用户浏览器 participant DS as DispatcherServlet（前台接待员） participant HM as HandlerMapping（路由查找器） participant HI as HandlerInterceptor（拦截器） participant HA as HandlerAdapter（适配器） participant HC as Handler/Controller（真正干活的） participant MAR as MessageConverter（数据转换器） participant VR as ViewResolver（视图解析器） participant V as View（模板/页面） 用户浏览器-\u003e\u003eDS: ① HTTP 请求（GET /home） DS-\u003e\u003eHM: ② 我收到请求了，谁来处理？ HM--\u003e\u003eDS: ③ 返回 HandlerExecutionChain（Handler + 拦截器列表） DS-\u003e\u003eHI: ④ preHandle()前置拦截（登录校验、日志等） alt 拦截器返回 false HI--\u003e\u003e用户浏览器: 直接拦截返回（如跳转登录页） end DS-\u003e\u003eHA: ⑤ 找到能处理这个 Handler 的适配器（supports() 方法匹配） HA-\u003e\u003eHC: ⑥ 调用 Handler（handle() 统一入口） note over HC: 执行业务逻辑调用 Service / DAO HC--\u003e\u003eHA: ⑦ 返回结果（ModelAndView 或 @ResponseBody 数据） HA--\u003e\u003eDS: ⑧ 返回 ModelAndView DS-\u003e\u003eHI: ⑨ postHandle()后置拦截（可修改 ModelAndView） alt 返回 @ResponseBody（REST接口） DS-\u003e\u003eMAR: ⑩ 用 MessageConverter 把对象序列化为 JSON/XML MAR--\u003e\u003e用户浏览器: ⑪ 直接写入响应体返回 else 返回视图名（传统MVC） DS-\u003e\u003eVR: ⑩ 解析视图名 → 找到模板文件 VR--\u003e\u003eDS: ⑪ 返回 View 对象 DS-\u003e\u003eV: ⑫ 渲染视图（填充 Model 数据） V--\u003e\u003e用户浏览器: ⑬ 返回 HTML 页面 end DS-\u003e\u003eHI: ⑭ afterCompletion()最终回调（资源清理、异常记录） Spring 的事务是如何回滚的 🥇 第一层：一句话定性（开场） \u0026ldquo;Spring 的事务回滚，底层是基于 AOP 实现的。@Transactional 本质上是一个 Around Advice，Spring 在方法执行前开启事务，方法正常返回则提交，如果捕获到异常则触发回滚。具体的事务操作委托给 PlatformTransactionManager 来执行，和底层数据库交互。\u0026rdquo;\n🥈 第二层：说清楚完整流程（核心） \u0026ldquo;具体流程是这样的——\u0026rdquo;\nflowchart TD A[\"调用 @Transactional 方法\"] --\u003e B subgraph TI [\"TransactionInterceptor\"] B[\"invoke(MethodInvocation invocation)\"] end B --\u003e C subgraph TAS [\"TransactionAspectSupport\"] C[\"invokeWithinTransaction()\"] C --\u003e D[\"createTransactionIfNecessary()\\n获取或创建事务\"] D --\u003e E[\"invocation.proceedWithInvocation()\\n执行目标方法\"] E --\u003e F{结果} F --\u003e|正常返回| G[\"commitTransactionAfterReturning()\"] F --\u003e|抛出异常| H[\"completeTransactionAfterThrowing()\\n判断是否符合回滚规则\"] H --\u003e|符合回滚规则| I[\"rollbackOnException()\\n→ rollback()\"] H --\u003e|不符合回滚规则| J[\"commit()\"] E --\u003e K[\"finally:\\ncleanupTransactionInfo()\"] end subgraph APTM [\"AbstractPlatformTransactionManager\\n（DataSourceTransactionManager 实现）\"] D --\u003e L[\"doBegin()\\nconnection.setAutoCommit(false)\"] G --\u003e M[\"doCommit()\"] I --\u003e N[\"doRollback()\"] J --\u003e M end style B fill:#f39c12,color:#fff style C fill:#f39c12,color:#fff style D fill:#8e44ad,color:#fff style L fill:#8e44ad,color:#fff style E fill:#2ecc71,color:#000 style G fill:#27ae60,color:#fff style M fill:#27ae60,color:#fff style H fill:#c0392b,color:#fff style I fill:#e74c3c,color:#fff style N fill:#e74c3c,color:#fff style J fill:#27ae60,color:#fff style K fill:#7f8c8d,color:#fff \u0026ldquo;判断是否回滚，Spring 默认只对 RuntimeException 和 Error 回滚，受检异常（checked Exception）默认不回滚。\u0026rdquo;\n🥉 第三层：说清楚回滚规则配置（细节） \u0026ldquo;回滚规则可以手动配置——\u0026rdquo;\n// 指定某个受检异常也要回滚 @Transactional(rollbackFor = Exception.class) // 指定某个异常不回滚 @Transactional(noRollbackFor = IllegalArgumentException.class) \u0026ldquo;Spring 内部用 RollbackRuleAttribute 来匹配异常类型，遍历异常继承链，找到最近的匹配规则来决定是提交还是回滚。\u0026rdquo;\n💡 主动抛出经典坑点，拉开差距 坑一：自调用导致事务失效（和 AOP 同根同源）\n\u0026ldquo;同类内部方法互调，@Transactional 不生效，原因和 AOP 自调用失效一样——绕过了代理对象。\u0026rdquo;\n@Service public class OrderService { public void placeOrder() { this.pay(); // ❌ 事务不生效，走的是原始对象 } @Transactional public void pay() { ... } } 坑二：异常被吃掉，事务无法感知\n\u0026ldquo;如果在方法内部把异常 try-catch 吃掉了，TransactionInterceptor 捕获不到异常，就不会触发回滚。\u0026rdquo;\n@Transactional public void pay() { try { db.update(...); } catch (Exception e) { log.error(\u0026#34;error\u0026#34;, e); // ❌ 异常被吃，事务照常提交 } } \u0026ldquo;解决方法：catch 后手动标记回滚——\u0026rdquo;\ncatch (Exception e) { TransactionAspectSupport.currentTransactionStatus() .setRollbackOnly(); // ✅ 手动触发回滚 } 坑三：受检异常默认不回滚\n@Transactional public void pay() throws IOException { throw new IOException(\u0026#34;文件不存在\u0026#34;); // ❌ 默认不回滚！ } // 正确做法： @Transactional(rollbackFor = Exception.class) // ✅ 追问预案 面试官追问 应对方向 事务传播机制说一下？ REQUIRED（默认，加入或新建）/ REQUIRES_NEW（挂起外层，新建）/ NESTED（嵌套，savepoint）等 7 种，重点说前三 REQUIRES_NEW 和 NESTED 的区别？ REQUIRES_NEW 是完全独立的新事务，外层回滚不影响它；NESTED 是嵌套在外层事务里，外层回滚会带着它一起滚 Spring 事务和数据库事务的关系？ Spring 事务是对数据库连接 connection 的封装管理，最终还是靠数据库的 ACID 保证，Spring 只是控制了 commit / rollback 的时机 多线程下 @Transactional 还有效吗？ 无效。Spring 事务通过 ThreadLocal 绑定当前线程的 connection，子线程拿不到同一个 connection，事务无法传播 @Transactional 加在接口上有效吗？ 不推荐。JDK 代理下勉强可以，CGLIB 代理下完全无效，Spring 官方建议始终加在实现类上 ❌ 常见回答误区 ❌ \u0026#34;Spring 事务就是加了个注解，自动提交和回滚\u0026#34; → 没说出 AOP 代理、TransactionInterceptor、连接管理这些核心机制 ❌ \u0026#34;所有异常都会回滚\u0026#34; → 经典错误，默认只回滚 RuntimeException 和 Error ❌ \u0026#34;事务传播只知道 REQUIRED\u0026#34; → 至少要能说出 REQUIRES_NEW 和 NESTED 及其区别 说一说 Spring 的事务传播 一、 核心必会（最常用，决定生死存亡） REQUIRED (默认行为)\n一句话概括：同生共死。\n逻辑：如果外层有事务，就加入它；如果没有，就自己新建一个。只要其中一个报错，全盘回滚。\nREQUIRES_NEW\n一句话概括：各过各的。\n逻辑：不管外层有没有事务，都必须挂起外层，自己开启一个全新的独立事务。两者互不干扰，适合做日志记录。\nNESTED\n一句话概括：长幼有序。\n逻辑：在外层事务中建立一个“保存点（Savepoint）”。子事务失败了可以单独回滚，不影响外层；但外层如果回滚，子事务必须跟着一起回滚。\n二、 温和顺从（顺应外层环境） SUPPORTS\n一句话概括：随缘吃席。\n逻辑：外层有事务，我就加入事务运行；外层没有事务，我就以非事务（普通方法）方式运行。\nNOT_SUPPORTED\n一句话概括：拒绝被卷。\n逻辑：不支持事务。如果外层有事务，先把外层事务挂起/暂停，自己以非事务方式运行完了，再让外层事务继续。\n三、 强硬排他（极端的规则破坏者） MANDATORY\n一句话概括：没票别进。\n逻辑：强制要求外层必须有事务，如果没有，直接抛出异常（IllegalTransactionStateException）。\nNEVER\n一句话概括：绝不沾毒。\n逻辑：坚决不支持事务。如果外层有事务，直接抛出异常；只有外层没有事务时，它才愿意正常运行。\nSpring框架中的单例bean是线程安全的吗？ 不是线程安全的\n有状态不安全 无状态安全 什么是AOP，你们项目有没有使用到AOP？ Aspect-Oriented Programming\nAOP使用场景：\n记录操作日志 缓存处理 spring中内置的事务处理 Spring中的事务是如何实现的？ 事务失效的场景有哪些？ 异常捕获处理 抛出检查异常 @Transactional 哪些场景事务无法回滚（基本同上） 抛出受检异常 只捕获异常，没有抛出 非public方法 事务传播行为导致不会回滚 必须遵守哪些规则让事务能够正常回滚？ public 方法 + rollbackFor = Exception.class 不用 this 自调用，保证走 AOP 代理（为什么 this 子调用无法走AOP代理？） 捕获异常一定要重新抛出，不吞异常 方法内无 DDL 语句、不手动 commit 不在子线程执行 DB 操作 使用 REQUIRED 默认传播级别 表引擎 InnoDB 为什么 this 子调用无法走AOP代理？ Spring AOP 是动态代理实现；this 指向原始目标对象，不是代理对象，不会经过代理拦截器，所以事务逻辑不会生效。\nSpring的bean的生命周期 BeanDefinition 构造函数 依赖注入 Aware接口 BeanPostProcessor before 初始化方法 BeanPostProcessor after AOP 动态代理 销毁 Bean Spring的循环引用？ 构造方法出现了循环依赖怎么解决？ @Lazy\nSpringMVC的执行流程 视图阶段/前后端分离阶段\nDispatcherServlet HandlerMapping 拦截器注意 HandlerAdapter Handler/Controller ViewReslover SpringBoot 自动配置原理 @SpringBootApplication @SpringBootConfiguration = @Configuration @EnableAutoConfiguration @Import @ComponentScan Spring框架常见注解 Spring @Component @Controller @Service @Repository @Autowired 类型 @Qualifier 名称 @Scope singleton prototype @Configuration @ComponentScan @Bean @Import AOP @Aspect @Before @After @Around @Pointcut SpringMVC @RequestMapping @GetMapping @PutMapping \u0026hellip; @RequestBody JSON 2 JAVA @RequestParam @PathVariable @ResponseBody JAVA -\u0026gt; JSON @RequestHeader @RestController @Controller @ResponseBody SpringBoot @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan ","permalink":"https://lv-blog.pages.dev/posts/programming/for-interview/spring-interview-questions/","summary":"\u003cblockquote\u003e\n\u003cp\u003e面试题从互联网各个角落收集而来\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"谈一谈-spring-ioc-的底层实现\"\u003e谈一谈 Spring IOC 的底层实现？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e反射\u003c/li\u003e\n\u003cli\u003e工厂的价值\u003c/li\u003e\n\u003cli\u003e设计模式\u003c/li\u003e\n\u003cli\u003e关键的几个方法\n\u003cul\u003e\n\u003cli\u003ecreateBeanFactory\u003c/li\u003e\n\u003cli\u003egetBean\u003c/li\u003e\n\u003cli\u003edoGetBean\u003c/li\u003e\n\u003cli\u003ecreateBean\u003c/li\u003e\n\u003cli\u003edoCreateBean\u003c/li\u003e\n\u003cli\u003ecreateBeanInstance(getDeclaredConstructor, newInstance)「方案选单」\u003c/li\u003e\n\u003cli\u003epopulateBean\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cpre class=\"mermaid\"\u003eflowchart TD\n    A[开始] --\u003e B[\"createBeanFactory()\u003cbr/\u003e创建 DefaultListableBeanFactory\"]\n    B --\u003e C[加载 BeanDefinition\u003cbr/\u003e解析 XML/注解]\n    C --\u003e D[\"getBean(beanName)\u003cbr/\u003e外部调用入口\"]\n\n    D --\u003e E[\"doGetBean(beanName)\"]\n    E --\u003e F{从缓存获取单例?}\n    F --\u003e|是| G[返回缓存的 Bean]\n    F --\u003e|否| H[\"createBean(beanName, mbd)\"]\n\n    H --\u003e I[\"doCreateBean(beanName, mbd)\"]\n    I --\u003e J[\"createBeanInstance()\u003cbr/\u003egetDeclaredConstructor + newInstance\"]\n    J --\u003e K[\"populateBean()\u003cbr/\u003e属性填充/依赖注入\"]\n    K --\u003e L[\"initializeBean()\u003cbr/\u003e初始化回调\"]\n    L --\u003e M[注册销毁方法]\n    M --\u003e N[返回完整 Bean 实例]\n\u003c/pre\u003e\n\u003ch2 id=\"谈谈-springioc-的理解原理和实现\"\u003e谈谈 SpringIOC 的理解，原理和实现？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003eIOC 思想\u003c/li\u003e\n\u003cli\u003eDI 实现手段\u003c/li\u003e\n\u003cli\u003e什么是容器 什么是Bean\u003c/li\u003e\n\u003cli\u003eBeanDefination\n\u003cul\u003e\n\u003cli\u003e从哪里读\n\u003cul\u003e\n\u003cli\u003eXML\u003c/li\u003e\n\u003cli\u003e注解\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e存哪里\n\u003cul\u003e\n\u003cli\u003e所有 BeanDefinition 都存在 DefaultListableBeanFactory 里的一个 Map 中\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"容器的生命周期\"\u003e容器的生命周期\u003c/h2\u003e\n\u003cpre class=\"mermaid\"\u003eflowchart TD\n    START([🚀 程序启动]) --\u003e REFRESH[\"AbstractApplicationContext.refresh()\"]\n\n    REFRESH --\u003e PHASE1\n\n    subgraph PHASE1[\"🔵 第一阶段：准备工作\"]\n        direction TB\n        A1[\"prepareRefresh()\\n记录启动时间\\n设置容器状态为「活跃」\\n初始化环境变量\"] --\u003e A2\n        A2[\"obtainFreshBeanFactory()\\n创建 DefaultListableBeanFactory\\n这是容器的核心仓库\"] --\u003e A3\n        A3[\"prepareBeanFactory()\\n注册基础 BeanPostProcessor\\n注册 Aware 相关处理器\\n注册默认环境 Bean（environment）\"]\n    end\n\n    PHASE1 --\u003e PHASE2\n\n    subgraph PHASE2[\"🟢 第二阶段：加载 BeanDefinition（读图纸）\"]\n        direction TB\n        B1[\"扫描 @ComponentScan 指定的包\\n或读取 XML 配置文件\"] --\u003e B2\n        B2[\"解析注解 / XML\\n识别 @Component @Service\\n@Repository @Controller @Bean\"] --\u003e B3\n        B3[\"为每个 Bean 生成 BeanDefinition\\n记录：类名、作用域、是否懒加载\\n初始化方法、销毁方法、依赖关系\"] --\u003e B4\n        B4[(\"存入 beanDefinitionMap\\nkey = beanName\\nvalue = BeanDefinition\")]\n    end\n\n    PHASE2 --\u003e PHASE3\n\n    subgraph PHASE3[\"🟡 第三阶段：修改 BeanDefinition（修图纸）\"]\n        direction TB\n        C1[\"invokeBeanFactoryPostProcessors()\\n执行所有 BeanFactoryPostProcessor\"] --\u003e C2\n        C2[\"ConfigurationClassPostProcessor\\n处理 @Configuration @Import\\n@PropertySource @ComponentScan\\n👉 SpringBoot 自动装配在此触发\"] --\u003e C3\n        C3[\"PropertySourcesPlaceholderConfigurer\\n替换 BeanDefinition 中\\n所有 ${xxx} 占位符为真实值\"] --\u003e C4\n        C4[/\"所有 BeanDefinition 最终确定\\n图纸不再变动\"/]\n    end\n\n    PHASE3 --\u003e PHASE4\n\n    subgraph PHASE4[\"🌸 第四阶段：注册 BeanPostProcessor（工人就位）\"]\n        direction TB\n        D1[\"registerBeanPostProcessors()\\n按优先级顺序注册\\nPriorityOrdered → Ordered → 普通\"] --\u003e D2\n        D2[\"AutowiredAnnotationBeanPostProcessor\\n负责处理 @Autowired @Value\"] --\u003e D3\n        D3[\"CommonAnnotationBeanPostProcessor\\n负责处理 @PostConstruct @PreDestroy @Resource\"] --\u003e D4\n        D4[\"AnnotationAwareAspectJAutoProxyCreator\\n负责检测切面、生成 AOP 代理对象\"] --\u003e D5\n        D5[/\"所有工人就位\\n等待 Bean 创建时介入\\n此时不开工\"/]\n    end\n\n    PHASE4 --\u003e PHASE5\n\n    subgraph PHASE5[\"🟣 第五阶段：容器基础设施初始化\"]\n        direction TB\n        E1[\"initMessageSource()\\n初始化国际化资源 i18n\"] --\u003e E2\n        E2[\"initApplicationEventMulticaster()\\n初始化事件广播器\"] --\u003e E3\n        E3[\"onRefresh()\\n⭐ SpringBoot 在此启动\\nTomcat / Jetty / Undertow\\nWeb 容器开始监听端口\"] --\u003e E4\n        E4[\"registerListeners()\\n注册所有 ApplicationListener\\n监听容器事件\"]\n    end\n\n    PHASE5 --\u003e PHASE6\n\n    subgraph PHASE6[\"🟠 第六阶段：实例化所有单例 Bean\"]\n        direction TB\n        F1[\"finishBeanFactoryInitialization()\\npreInstantiateSingletons()\\n遍历所有非懒加载单例 BeanDefinition\"] --\u003e F2\n        F2[\"逐个执行 getBean()\\n触发每个 Bean 的生命周期\\n实例化 → 属性填充 → Aware回调\\n→ 前置处理 → init方法 → 后置处理\"] --\u003e F3\n        F3[(\"所有单例 Bean 存入\\nsingletonObjects 一级缓存\\n✅ 全部就绪\")]\n    end\n\n    PHASE6 --\u003e PHASE7\n\n    subgraph PHASE7[\"✅ 第七阶段：容器就绪\"]\n        direction TB\n        G1[\"finishRefresh()\\n清理启动时占用的资源\\n初始化生命周期处理器\"] --\u003e G2\n        G2[\"发布 ContextRefreshedEvent\\n通知所有监听者：容器启动完成\"] --\u003e G3\n        G3[/\"容器进入运行状态\\n可以处理业务请求\"/]\n    end\n\n    G3 --\u003e RUNNING([🎉 容器正常运行中])\n\n    RUNNING -.-\u003e|\"收到关闭信号\\nCtrl+C / kill / close()\"| PHASE8\n\n    subgraph PHASE8[\"🔴 第八阶段：容器销毁\"]\n        direction TB\n        H1[\"发布 ContextClosedEvent\\n通知所有监听者：容器即将关闭\"] --\u003e H2\n        H2[\"停止 Web 容器\\nTomcat 停止接受新请求\"] --\u003e H3\n        H3[\"执行所有 Bean 的销毁逻辑\\n@PreDestroy → destroy() → destroy-method\\n按注册顺序逆序销毁\"] --\u003e H4\n        H4[\"清空所有缓存\\nsingletonObjects 清空\\nbeanDefinitionMap 清空\"] --\u003e H5\n        H5[\"容器状态设置为「关闭」\\nactive = false / closed = true\"]\n    end\n\n    H5 --\u003e END([💀 容器销毁完成])\n\n    style START fill:#43A047,color:#fff\n    style RUNNING fill:#43A047,color:#fff\n    style END fill:#e53935,color:#fff\n    style REFRESH fill:#1E88E5,color:#fff\n\u003c/pre\u003e\n\u003ch2 id=\"bean-的生命周期\"\u003ebean 的生命周期\u003c/h2\u003e\n\u003cpre class=\"mermaid\"\u003eflowchart TD\n    START([🌱 开始创建 Bean]) --\u003e A\n\n    A[\"① 实例化\\nInstantiation\\n反射调用构造方法\\nnew 出空壳对象\\n此时所有字段都是 null\"] --\u003e B\n\n    B[\"② 放入三级缓存\\nsingletonFactories\\n提前暴露半成品\\n为循环依赖做准备\"] --\u003e C\n\n    C[\"③ 属性填充\\npopulateBean()\\n处理 @Autowired @Value\\n把依赖的 Bean 注入进来\"] --\u003e D\n\n    subgraph AWARE[\"④ Aware 回调\"]\n        direction TB\n        D[\"BeanNameAware\\n告诉 Bean 自己叫什么名字\"] --\u003e E\n        E[\"BeanFactoryAware\\n把 BeanFactory 塞给 Bean\"] --\u003e F\n        F[\"ApplicationContextAware\\n把 ApplicationContext 塞给 Bean\"]\n    end\n\n    subgraph INIT[\"⑤ 初始化 initializeBean()\"]\n        direction TB\n        G[\"前置处理\\npostProcessBeforeInitialization()\\n所有 BeanPostProcessor 挨个执行\\n📌 @PostConstruct 在这里被调用\"] --\u003e INITMETHOD\n\n        subgraph INITMETHOD[\"init 方法（按顺序执行）\"]\n            direction TB\n            H1[\"第1个：@PostConstruct\\n（已在前置处理中执行）\"] --\u003e H2\n            H2[\"第2个：afterPropertiesSet()\\n实现 InitializingBean 接口\"] --\u003e H3\n            H3[\"第3个：init-method\\n@Bean(initMethod='xxx') 或 XML 配置\"]\n        end\n\n        INITMETHOD --\u003e I\n\n        I[\"后置处理\\npostProcessAfterInitialization()\\n所有 BeanPostProcessor 挨个执行\\n⭐ AOP 代理在这里生成\"]\n    end\n\n    AWARE --\u003e INIT\n\n    I --\u003e J\n\n    subgraph CACHE[\"⑥ 存入缓存\"]\n        direction TB\n        J[\"从三级缓存 singletonFactories 移除\\n从二级缓存 earlySingletonObjects 移除\"] --\u003e K\n        K[\"存入一级缓存 singletonObjects\\n✅ 完整 Bean 就绪\"]\n    end\n\n    K --\u003e READY([🎉 Bean 可以正常使用了])\n\n    READY -.-\u003e|\"容器关闭\"| DESTROY\n\n    subgraph DESTROY[\"⑦ 销毁阶段（容器关闭时）\"]\n        direction TB\n        D1[\"第1个：@PreDestroy\\n方法被调用\"] --\u003e D2\n        D2[\"第2个：destroy()\\n实现 DisposableBean 接口\"] --\u003e D3\n        D3[\"第3个：destroy-method\\n@Bean(destroyMethod='xxx') 或 XML 配置\"]\n    end\n\n    D3 --\u003e END([💀 Bean 销毁完成])\n\u003c/pre\u003e\n\u003ch2 id=\"spring-三级缓存依赖流程\"\u003espring 三级缓存依赖流程\u003c/h2\u003e\n\u003cpre class=\"mermaid\"\u003esequenceDiagram\n    participant Spring as Spring容器\n    participant L3 as 三级缓存\u003cbr/\u003esingletonFactories\n    participant L2 as 二级缓存\u003cbr/\u003eearlySingletonObjects\n    participant L1 as 一级缓存\u003cbr/\u003esingletonObjects\n\n    Note over Spring: 开始创建 A\n\n    Spring-\u003e\u003eSpring: ① new A 空壳对象\n    Spring-\u003e\u003eL3: ② 存入 A 的 ObjectFactory lambda\n\n    Note over Spring: 给 A 注入属性，发现需要 B\n\n    Spring-\u003e\u003eSpring: ③ new B 空壳对象\n    Spring-\u003e\u003eL3: ④ 存入 B 的 ObjectFactory lambda\n\n    Note over Spring: 给 B 注入属性，发现需要 A\n\n    Spring-\u003e\u003eL1: ⑤ getBean(A)，查一级缓存\n    L1--\u003e\u003eSpring: ❌ 没有\n\n    Spring-\u003e\u003eL2: 查二级缓存\n    L2--\u003e\u003eSpring: ❌ 没有\n\n    Spring-\u003e\u003eL3: 查三级缓存\n    L3--\u003e\u003eSpring: ✅ 找到 A 的 lambda！\n\n    Spring-\u003e\u003eSpring: ⑥ 执行 lambda\u003cbr/\u003e（需要代理则生成代理 A）\n    Spring-\u003e\u003eL2: ⑦ 早期引用放入二级缓存\n    Spring-\u003e\u003eL3: 删除三级缓存中的 A\n\n    Note over Spring: B 拿到 A 的早期引用，B 完成初始化\n\n    Spring-\u003e\u003eL1: ⑧ B 放入一级缓存 ✅\n    Spring-\u003e\u003eL3: 删除三级缓存中的 B\n\n    Note over Spring: 回到 A 的流程，B 已就绪，A 完成初始化\n\n    Spring-\u003e\u003eL1: ⑨ A 放入一级缓存 ✅\n    Spring-\u003e\u003eL2: 删除二级缓存中的 A\n\n    Note over L1: 最终：一级缓存中有完整的 A 和 B ✅\n\u003c/pre\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e情况\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e三级缓存\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003elambda 执行\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e二级缓存\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e无循环依赖，无 AOP\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e存了 lambda\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e❌ 不执行\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e不经过\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e无循环依赖，有 AOP\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e存了 lambda\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e❌ 不执行\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e不经过\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e有循环依赖，无 AOP\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e存了 lambda\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e✅ 执行\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e存原始对象\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e有循环依赖，有 AOP\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e存了 lambda\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e✅ 执行\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e存代理对象\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"spring-bean-缓存的放置时间和删除时间\"\u003espring bean 缓存的放置时间和删除时间\u003c/h2\u003e\n\u003ch2 id=\"spring-bean-三级缓存的放置与删除时间\"\u003eSpring Bean 三级缓存的放置与删除时间\u003c/h2\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e缓存\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e存的是什么\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e放入时机\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e删除时机\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e三级 \u003ccode\u003esingletonFactories\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eBean 的工厂 lambda\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e实例化完成后立刻放入\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e工厂被调用时（升级到二级）或 Bean 完成时\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e二级 \u003ccode\u003eearlySingletonObjects\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e早期暴露的半成品 Bean\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e三级工厂被调用的瞬间\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eBean 完全初始化完成放入一级时\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e一级 \u003ccode\u003esingletonObjects\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e完整可用的 Bean\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e初始化全部完成后\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e容器关闭销毁时\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch2 id=\"beanfactory-和-factorybean-的区别\"\u003eBeanFactory 和 FactoryBean 的区别？\u003c/h2\u003e\n\u003ch3 id=\"factorybean-是什么\"\u003eFactoryBean 是什么\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003einterface\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eFactoryBean\u003c/span\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003eT\u003cspan style=\"color:#f92672\"\u003e\u0026gt;\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 返回 Bean 的实例（可以是复杂创建逻辑）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    T \u003cspan style=\"color:#a6e22e\"\u003egetObject\u003c/span\u003e() \u003cspan style=\"color:#66d9ef\"\u003ethrows\u003c/span\u003e Exception;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 返回 Bean 的类型\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    Class\u003cspan style=\"color:#f92672\"\u003e\u0026lt;?\u0026gt;\u003c/span\u003e getObjectType();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#75715e\"\u003e// 是否单例\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003edefault\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eboolean\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eisSingleton\u003c/span\u003e() {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e        \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003etrue\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    }\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e相同点\n\u003cul\u003e\n\u003cli\u003e都是用来创建Bean对象的\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e不同点\n\u003cul\u003e\n\u003cli\u003e使用 BeanFactory 创建对象的适合必须遵循严格的生命周期流程，太复杂了。如果想简单自定义某个对象的创建，同时想交给spring管理，那么必须实现 FactoryBean 接口\n\u003cul\u003e\n\u003cli\u003eisSingleton 是否是单例对象\u003c/li\u003e\n\u003cli\u003egetObjectType 获取返回对象的类型\u003c/li\u003e\n\u003cli\u003egetObject 自定义创建对象的过程\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e对比维度\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eBeanFactory\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eFactoryBean\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e角色\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e容器/工厂接口\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e特殊的 Bean\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e功能\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e管理所有 Bean 的生命周期\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e自定义某个 Bean 的创建逻辑\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e定位\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e基础设施（IOC 容器）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e业务扩展（创建复杂对象）\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e使用方式\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e由 Spring 框架实现和使用\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e由开发者实现，注册到容器中\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e常见实现\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eDefaultListableBeanFactory\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eSqlSessionFactoryBean\u003c/code\u003e、\u003ccode\u003eProxyFactoryBean\u003c/code\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e谁创建谁\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eBeanFactory\u003c/code\u003e 创建并管理 \u003ccode\u003eFactoryBean\u003c/code\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003ccode\u003eFactoryBean\u003c/code\u003e 创建业务对象\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003chr\u003e\n\u003cpre class=\"mermaid\"\u003eflowchart LR\n    subgraph BeanFactory[BeanFactory - IOC容器]\n        direction LR\n        Bean1[普通 Bean]\n        Bean2[普通 Bean]\n        FB[FactoryBean\u003cbr/\u003e特殊 Bean]\n    end\n\n    FB --\u003e|调用 getObject| Result[业务对象]\n\n    User[开发者] --\u003e|getBean| BF[BeanFactory]\n    BF --\u003e|返回| Bean1\n    BF --\u003e|返回| Bean2\n    BF --\u003e|返回 getObject 结果| Result\n    BF --\u003e|加 \u0026 前缀| FB\n\u003c/pre\u003e\n\u003ch2 id=\"spring中用到的设计模式\"\u003eSpring中用到的设计模式\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e单例模式\u003c/li\u003e\n\u003cli\u003e原型模式（指定作用域为prototype）\u003c/li\u003e\n\u003cli\u003e工厂模式\n\u003cul\u003e\n\u003cli\u003eBeanFactory\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e模板方法\n\u003cul\u003e\n\u003cli\u003eJdbcTemplate\u003c/li\u003e\n\u003cli\u003eTransactionTemplate\u003c/li\u003e\n\u003cli\u003eRestTemplate\u003c/li\u003e\n\u003cli\u003eRedisTemplate\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e策略模式\n\u003cul\u003e\n\u003cli\u003eXmlBeanDefinitionReader\u003c/li\u003e\n\u003cli\u003ePropertiesBeanDefinitionReader\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e观察者模式\n\u003cul\u003e\n\u003cli\u003elistener\u003c/li\u003e\n\u003cli\u003eevent\u003c/li\u003e\n\u003cli\u003emulticast\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e适配器模式\n\u003cul\u003e\n\u003cli\u003eHandlerAdapter\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e装饰者模式\n\u003cul\u003e\n\u003cli\u003eBeanWrapper\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e责任链模式\n\u003cul\u003e\n\u003cli\u003e使用aop的时候会先生成一个拦截器链\u003c/li\u003e\n\u003cli\u003eSpringMVC 的filter责任链\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e代理模式\n\u003cul\u003e\n\u003cli\u003e动态代理\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e委托者模式\n\u003cul\u003e\n\u003cli\u003edelegate\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"spring-aop-底层实现原理\"\u003eSpring AOP 底层实现原理\u003c/h2\u003e\n\u003ch3 id=\"-第一层一句话定性开场\"\u003e🥇 第一层：一句话定性（开场）\u003c/h3\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u0026ldquo;Spring AOP 的底层本质是\u003cstrong\u003e动态代理\u003c/strong\u003e。Spring 在容器初始化 Bean 的时候，通过 \u003ccode\u003eBeanPostProcessor\u003c/code\u003e 机制拦截，判断这个 Bean 是否需要被增强，如果需要，就用动态代理生成一个代理对象，替换掉原始 Bean 注册进容器。后续所有对这个 Bean 的调用，实际上都是在走代理对象。\u0026rdquo;\u003c/p\u003e","title":"Spring 面试题整理"},{"content":"我目前使用的电钢琴是卡哇伊（KAWAI）ES105（据网友称为未阉割半踏版本）。今天路过南京一家琴行，顺便试弹了四款电钢琴：\nROLAND FP30X YAMAHA P-125 KAWAI CA-30 ROLAND HP704 一、整体第一印象：差异主要在音响，而非键盘 试弹四款琴后，我的第一感受是：\n它们之间最大的差异并不在键盘手感，而是在外放音质。\n1）外放音质对比 YAMAHA P-125 整体音质最弱（但仍在“可用可听”范围内）。 主要问题是低频明显不足，声音偏薄，但不至于刺耳或难以接受。\nROLAND HP704 外放表现最好。低频量感更足，整体更饱满。 但说实话，它并没有带来那种“震撼级”的听感提升。\n一个有意思的对比是： 如果让我用索尼 N3AP 监听耳机接在我的 KAWAI ES105 上 vs HP704 外放，我反而会更倾向前者的听感。\n二、键盘手感体验：FP30X 最接近立式钢琴 1）整体结论 在四款琴中：\nROLAND FP30X 的键盘手感最接近立式钢琴。\n但它和真正立式钢琴的关键差异在于：\n琴键整体“偏轻”\n2）关于“键盘力度”的感受 我个人认为琴键的力度对演奏表现影响非常大，它类似于摄影中的“动态范围”概念：\n力度越真实、越重 → 越容易表达音乐的“张力”和“重量感” 过轻的键盘 → 更容易弹，但表现力上限受限 在老师的立式钢琴上弹奏时，我能明显感觉到：\n木质机械结构带来的“阻尼感” 以及它对音乐表现力的客观增强 3）关于日系 vs 欧系键盘重量的观察 常见说法是：\n欧系键盘更重 日系键盘更轻 我认为这可能与欧洲古典音乐对“力度层次”和“表现张力”的需求有关。\n不过轻键盘也有优势：\n回弹更快 更适合轮指、快速技巧类乐段 三、市场选择逻辑：为什么 FP30X 卖得好 琴行老师提到：\nFP30X 是卖得最好的型号之一\n我理解原因如下：\n手感更接近立式钢琴 琴童在家练习后，上课用真钢琴“割裂感更小” 不容易出现“换琴就不跟手”的问题 从实用角度看，这种一致性非常关键。\n四、进阶与入门的差异：是否值得为手感升级买单？ 坦白说，对于非专业学习者：\n大部分电钢琴的手感差异，其实不容易被明显感知。\n例如：\nFP30X、P-125、ES105 → 普通用户体验差异有限 HP704（PHA-50结构）→ 更精细，但提升主要体现在“专业细节层面” 类比理解 可以这样理解差异：\nES105 / P-125：约“90分水平” HP704：约“98分水平” 对于不同人群意义完全不同：\n普通学习者：90分已经足够优秀 专业/考级/演奏者：98分可能是关键差距 甚至在更高端领域（如跨界电钢、三角钢琴模拟），专业人士会说：\nHP704：“有点意思” YAMAHA N1X：才更接近三角钢琴 但这些差异，在普通听众甚至初学者耳中，往往是不可感知或不重要的。\n五、CA-30 与 HP704 的对比印象 在琴行里，我意外感觉：\nKAWAI CA-30 在手感上可以和 ROLAND HP704 “掰一掰手腕”\n两者差距并没有想象中那么悬殊。\n但由于时间关系（已经占用了老板较长时间），没有继续深入细弹对比。\n","permalink":"https://lv-blog.pages.dev/posts/piano/about-buying-electric-piano/","summary":"\u003cp\u003e我目前使用的电钢琴是卡哇伊（KAWAI）ES105（据网友称为未阉割半踏版本）。今天路过南京一家琴行，顺便试弹了四款电钢琴：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003eROLAND FP30X\u003c/li\u003e\n\u003cli\u003eYAMAHA P-125\u003c/li\u003e\n\u003cli\u003eKAWAI CA-30\u003c/li\u003e\n\u003cli\u003eROLAND HP704\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"一整体第一印象差异主要在音响而非键盘\"\u003e一、整体第一印象：差异主要在音响，而非键盘\u003c/h2\u003e\n\u003cp\u003e试弹四款琴后，我的第一感受是：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e它们之间最大的差异并不在键盘手感，而是在外放音质。\u003c/strong\u003e\u003c/p\u003e\n\u003ch3 id=\"1外放音质对比\"\u003e1）外放音质对比\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eYAMAHA P-125\u003c/strong\u003e\n整体音质最弱（但仍在“可用可听”范围内）。\n主要问题是低频明显不足，声音偏薄，但不至于刺耳或难以接受。\u003c/p\u003e\n\u003c/li\u003e\n\u003cli\u003e\n\u003cp\u003e\u003cstrong\u003eROLAND HP704\u003c/strong\u003e\n外放表现最好。低频量感更足，整体更饱满。\n但说实话，它并没有带来那种“震撼级”的听感提升。\u003c/p\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e一个有意思的对比是：\n如果让我用索尼 N3AP 监听耳机接在我的 KAWAI ES105 上 vs HP704 外放，我反而会更倾向前者的听感。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"二键盘手感体验fp30x-最接近立式钢琴\"\u003e二、键盘手感体验：FP30X 最接近立式钢琴\u003c/h2\u003e\n\u003ch3 id=\"1整体结论\"\u003e1）整体结论\u003c/h3\u003e\n\u003cp\u003e在四款琴中：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eROLAND FP30X 的键盘手感最接近立式钢琴。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e但它和真正立式钢琴的关键差异在于：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e琴键整体“偏轻”\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch3 id=\"2关于键盘力度的感受\"\u003e2）关于“键盘力度”的感受\u003c/h3\u003e\n\u003cp\u003e我个人认为琴键的力度对演奏表现影响非常大，它类似于摄影中的“动态范围”概念：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e力度越真实、越重 → 越容易表达音乐的“张力”和“重量感”\u003c/li\u003e\n\u003cli\u003e过轻的键盘 → 更容易弹，但表现力上限受限\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e在老师的立式钢琴上弹奏时，我能明显感觉到：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e木质机械结构带来的“阻尼感”\u003c/li\u003e\n\u003cli\u003e以及它对音乐表现力的客观增强\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch3 id=\"3关于日系-vs-欧系键盘重量的观察\"\u003e3）关于日系 vs 欧系键盘重量的观察\u003c/h3\u003e\n\u003cp\u003e常见说法是：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e欧系键盘更重\u003c/li\u003e\n\u003cli\u003e日系键盘更轻\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e我认为这可能与欧洲古典音乐对“力度层次”和“表现张力”的需求有关。\u003c/p\u003e\n\u003cp\u003e不过轻键盘也有优势：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e回弹更快\u003c/li\u003e\n\u003cli\u003e更适合轮指、快速技巧类乐段\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"三市场选择逻辑为什么-fp30x-卖得好\"\u003e三、市场选择逻辑：为什么 FP30X 卖得好\u003c/h2\u003e\n\u003cp\u003e琴行老师提到：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003eFP30X 是卖得最好的型号之一\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e我理解原因如下：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e手感更接近立式钢琴\u003c/li\u003e\n\u003cli\u003e琴童在家练习后，上课用真钢琴“割裂感更小”\u003c/li\u003e\n\u003cli\u003e不容易出现“换琴就不跟手”的问题\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e从实用角度看，这种一致性非常关键。\u003c/p\u003e","title":"晚上逛南京某琴行电钢琴心得体会"},{"content":" 面试题从互联网各个角落收集而来\n如何定位慢查询？ 方案1 开源工具 Arthas 运维工具 Prometheus Skywalking 方案2 MySQL自带慢日志(mysql性能损耗) 开启慢日志方法 /etc/my.conf slow_query_log long_query_time SQL语句执行很慢，如何分析 慢的原因 聚合查询 多表查询 表数据量过大查询 深度分页查询 如何分析慢 explain， desc 命令 如何分析执行结果？ extra 的额外优化建议 using where; using index 查找使用了索引，需要的数据在索引列都能找到，不需要回表查询数据 using index condition 查找使用了索引 type index，all 需要优化 了解过索引吗？什么是索引？ 索引（index）是帮助 MySQL 高效获取数据的数据结构（有序）。在数据之外，数据库系统还维护着满足特定查找算法的数据结构（如 B+ 树），这些数据结构以某种方式引用（指向）数据，这样就可以在这些数据结构上实现高级查找算法，这种数据结构就是索引。\n帮助MySQL高效获取数据的数据结构 索引的底层数据结构了解过吗？ 二叉搜索树 红黑树 B树 B+树 阶数更多，路径更短 磁盘读写代价B+树更低，叶子节点才能真正存储数据 便于扫库和区间查询，叶子节点是双向链表 什么是聚簇索引？什么是非聚簇索引？ 聚簇索引 二级索引 什么是回表查询？ 知道什么是覆盖索引吗？ 通过该索引查询能够一次找到所有数据，且无需回表的，就是覆盖索引\nMySQL超大分页怎么处理？ 使用覆盖索引加上子查询\nselect * from tb_sku t, (select id from tb_sku order by id limit 9000000,10) a where t.id = a.id 索引创建的原则有哪些？ 单表超过10万条数据且查询比较频繁的表建立索引 常常作为查询条件的字段要建立索引 使用区分度高的列作为索引，尽量建立唯一索引 字符串类型的字段长度较长可以建立前缀索引 尽量使用联合索引，减少单列索引 索引列使用NOT NULL方便优化器确定哪个索引更好用于查询 什么情况下索引会失效？ 违反了最左前缀法则 查询范围右边的列，不能使用索引 索引列上进行运算操作，索引列失效 字符串不加单引号，索引失效（索引类型转换导致的失效） 字符串非尾部匹配，索引失效 谈一谈对SQL优化的经验 表设计优化 我们参考了 阿里开发手册《嵩山版》 根据实际存储数值长短设计数据类型 索引的优化 SQL语句的优化 select 语句务必指明字段名称 为了覆盖索引 尽量用 union all 代替 union 避免对 where 子句中对字段进行表达式操作 能用 innerjoin 就不用 left join , right join。如必须，要以小表为驱动 主从复制，读写分离 分库分表 事务的特性详细说一说 并发事务带来哪些问题，如何解决这些问题？MySQL的默认隔离级别是？ 并发事务的问题 脏读 读已提交 不可重复读 值问题 可重复读 幻读 数量问题 串行化 隔离级别 未提交读 读已提交 可重复读* 串行化 undo log 和 redo log 的区别 缓冲池 数据页 redo log 物理 持久 undo log 逻辑 原子 一致 事务的隔离性底层是如何保证的？ 锁：排他锁 MVCC 多版本并发控制 依赖于 隐式字段 DB_TRX_ID DB_ROLL_PTR DB_ROW_ID undo log 版本链 版本链数据访问规则 readview 快照读 Read Committed 每次 select都生成一个快照读 Repeatable Read 开启事务后第一个 select 语句才是快照读的地方 核心字段 m_ids min_trx_id max_trx_id creator_trx_id RC隔离级别下，在事务中每次执行快照读时生成 ReadView MySQL 的主从同步原理 核心：二进制日志 BINLOG DDL DML 主数据库在事务提交时，变更记录写入 Binlog 从库 binlog -\u0026gt; 中继日志 从库 中继日志读取事件 -\u0026gt; 从库数据库 你们项目用过分库分表吗？ 什么时候分库分表 单表数据量达到1000W 或者 20GB 优化解决不了性能问题 IO、CPU瓶颈 如何拆分 垂直拆分 垂直分库 不同表拆分到不同库 适用于微服务 垂直分表 不常用字段单独放在一张表 水平拆分 水平分库 一个库的数据拆分到多个库中 路由规则 根据id节点取模 按id范围路由 水平分表 一个表的数据拆分到多个表中 新的问题 分布式事务一致性问题 跨节点关联查询 跨节点分页、排序函数 主键去重 解决方案 分库分表中间件 mycat sharding-sphere 深入问题 为什么 InnoDB 主键建议自增？ 为什么非自增主键会导致页分裂？ 为什么 MyISAM 和 InnoDB 索引结构不同？ 为什么 B+树适合磁盘而红黑树不适合？ ","permalink":"https://lv-blog.pages.dev/posts/programming/for-interview/mysql-interview-questions/","summary":"\u003cblockquote\u003e\n\u003cp\u003e面试题从互联网各个角落收集而来\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"如何定位慢查询\"\u003e如何定位慢查询？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e方案1\n\u003cul\u003e\n\u003cli\u003e开源工具\n\u003cul\u003e\n\u003cli\u003eArthas\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e运维工具\n\u003cul\u003e\n\u003cli\u003ePrometheus\u003c/li\u003e\n\u003cli\u003eSkywalking\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e方案2\n\u003cul\u003e\n\u003cli\u003eMySQL自带慢日志(mysql性能损耗)\n\u003cul\u003e\n\u003cli\u003e开启慢日志方法\n\u003cul\u003e\n\u003cli\u003e/etc/my.conf\n\u003cul\u003e\n\u003cli\u003eslow_query_log\u003c/li\u003e\n\u003cli\u003elong_query_time\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"sql语句执行很慢如何分析\"\u003eSQL语句执行很慢，如何分析\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e慢的原因\n\u003cul\u003e\n\u003cli\u003e聚合查询\u003c/li\u003e\n\u003cli\u003e多表查询\u003c/li\u003e\n\u003cli\u003e表数据量过大查询\u003c/li\u003e\n\u003cli\u003e深度分页查询\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e如何分析慢\n\u003cul\u003e\n\u003cli\u003eexplain， desc 命令\n\u003cul\u003e\n\u003cli\u003e如何分析执行结果？\n\u003cul\u003e\n\u003cli\u003eextra 的额外优化建议\n\u003cul\u003e\n\u003cli\u003eusing where; using index 查找使用了索引，需要的数据在索引列都能找到，不需要回表查询数据\u003c/li\u003e\n\u003cli\u003eusing index condition 查找使用了索引\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003etype\n\u003cul\u003e\n\u003cli\u003eindex，all 需要优化\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"了解过索引吗什么是索引\"\u003e了解过索引吗？什么是索引？\u003c/h2\u003e\n\u003cp\u003e索引（index）是帮助 MySQL 高效获取数据的数据结构（有序）。在数据之外，数据库系统还维护着满足特定查找算法的数据结构（如 B+ 树），这些数据结构以某种方式引用（指向）数据，这样就可以在这些数据结构上实现高级查找算法，这种数据结构就是索引。\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e帮助MySQL高效获取数据的数据结构\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"索引的底层数据结构了解过吗\"\u003e索引的底层数据结构了解过吗？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e二叉搜索树\u003c/li\u003e\n\u003cli\u003e红黑树\u003c/li\u003e\n\u003cli\u003eB树\u003c/li\u003e\n\u003cli\u003eB+树\n\u003cul\u003e\n\u003cli\u003e阶数更多，路径更短\u003c/li\u003e\n\u003cli\u003e磁盘读写代价B+树更低，叶子节点才能真正存储数据\u003c/li\u003e\n\u003cli\u003e便于扫库和区间查询，叶子节点是双向链表\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"什么是聚簇索引什么是非聚簇索引\"\u003e什么是聚簇索引？什么是非聚簇索引？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e聚簇索引\u003c/li\u003e\n\u003cli\u003e二级索引\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"什么是回表查询\"\u003e什么是回表查询？\u003c/h2\u003e\n\u003ch2 id=\"知道什么是覆盖索引吗\"\u003e知道什么是覆盖索引吗？\u003c/h2\u003e\n\u003cp\u003e通过该索引查询能够一次找到所有数据，且无需回表的，就是覆盖索引\u003c/p\u003e\n\u003ch2 id=\"mysql超大分页怎么处理\"\u003eMySQL超大分页怎么处理？\u003c/h2\u003e\n\u003cp\u003e使用覆盖索引加上子查询\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-mysql\" data-lang=\"mysql\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eselect\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003efrom\u003c/span\u003e tb_sku t,\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  (\u003cspan style=\"color:#66d9ef\"\u003eselect\u003c/span\u003e id \u003cspan style=\"color:#66d9ef\"\u003efrom\u003c/span\u003e tb_sku \u003cspan style=\"color:#66d9ef\"\u003eorder\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eby\u003c/span\u003e id \u003cspan style=\"color:#66d9ef\"\u003elimit\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e9000000\u003c/span\u003e,\u003cspan style=\"color:#ae81ff\"\u003e10\u003c/span\u003e) a \n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003ewhere\u003c/span\u003e t.id \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e a.id\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch2 id=\"索引创建的原则有哪些\"\u003e索引创建的原则有哪些？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e单表超过10万条数据且查询比较频繁的表建立索引\u003c/li\u003e\n\u003cli\u003e常常作为查询条件的字段要建立索引\u003c/li\u003e\n\u003cli\u003e使用区分度高的列作为索引，尽量建立唯一索引\u003c/li\u003e\n\u003cli\u003e字符串类型的字段长度较长可以建立前缀索引\u003c/li\u003e\n\u003cli\u003e尽量使用联合索引，减少单列索引\u003c/li\u003e\n\u003cli\u003e索引列使用NOT NULL方便优化器确定哪个索引更好用于查询\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"什么情况下索引会失效\"\u003e什么情况下索引会失效？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e违反了最左前缀法则\u003c/li\u003e\n\u003cli\u003e查询范围右边的列，不能使用索引\u003c/li\u003e\n\u003cli\u003e索引列上进行运算操作，索引列失效\u003c/li\u003e\n\u003cli\u003e字符串不加单引号，索引失效（索引类型转换导致的失效）\u003c/li\u003e\n\u003cli\u003e字符串非尾部匹配，索引失效\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"谈一谈对sql优化的经验\"\u003e谈一谈对SQL优化的经验\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e表设计优化\n\u003cul\u003e\n\u003cli\u003e我们参考了 阿里开发手册《嵩山版》\u003c/li\u003e\n\u003cli\u003e根据实际存储数值长短设计数据类型\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e索引的优化\u003c/li\u003e\n\u003cli\u003eSQL语句的优化\n\u003cul\u003e\n\u003cli\u003eselect 语句务必指明字段名称\n\u003cul\u003e\n\u003cli\u003e为了覆盖索引\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e尽量用 union all 代替 union\u003c/li\u003e\n\u003cli\u003e避免对 where 子句中对字段进行表达式操作\u003c/li\u003e\n\u003cli\u003e能用 innerjoin 就不用 left join , right join。如必须，要以小表为驱动\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e主从复制，读写分离\u003c/li\u003e\n\u003cli\u003e分库分表\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"事务的特性详细说一说\"\u003e事务的特性详细说一说\u003c/h2\u003e\n\u003ch2 id=\"并发事务带来哪些问题如何解决这些问题mysql的默认隔离级别是\"\u003e并发事务带来哪些问题，如何解决这些问题？MySQL的默认隔离级别是？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e并发事务的问题\n\u003cul\u003e\n\u003cli\u003e脏读\n\u003cul\u003e\n\u003cli\u003e读已提交\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e不可重复读\n\u003cul\u003e\n\u003cli\u003e值问题\u003c/li\u003e\n\u003cli\u003e可重复读\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e幻读\n\u003cul\u003e\n\u003cli\u003e数量问题\u003c/li\u003e\n\u003cli\u003e串行化\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e隔离级别\n\u003cul\u003e\n\u003cli\u003e未提交读\u003c/li\u003e\n\u003cli\u003e读已提交\u003c/li\u003e\n\u003cli\u003e可重复读*\u003c/li\u003e\n\u003cli\u003e串行化\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"undo-log-和-redo-log-的区别\"\u003eundo log 和 redo log 的区别\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e缓冲池\u003c/li\u003e\n\u003cli\u003e数据页\u003c/li\u003e\n\u003cli\u003eredo log\n\u003cul\u003e\n\u003cli\u003e物理\u003c/li\u003e\n\u003cli\u003e持久\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003eundo log\n\u003cul\u003e\n\u003cli\u003e逻辑\u003c/li\u003e\n\u003cli\u003e原子 一致\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"事务的隔离性底层是如何保证的\"\u003e事务的隔离性底层是如何保证的？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e锁：排他锁\u003c/li\u003e\n\u003cli\u003eMVCC\n\u003cul\u003e\n\u003cli\u003e多版本并发控制\u003c/li\u003e\n\u003cli\u003e依赖于\n\u003cul\u003e\n\u003cli\u003e隐式字段\n\u003cul\u003e\n\u003cli\u003eDB_TRX_ID\u003c/li\u003e\n\u003cli\u003eDB_ROLL_PTR\u003c/li\u003e\n\u003cli\u003eDB_ROW_ID\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003eundo log\n\u003cul\u003e\n\u003cli\u003e版本链\n\u003cul\u003e\n\u003cli\u003e版本链数据访问规则\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003ereadview\n\u003cul\u003e\n\u003cli\u003e快照读\n\u003cul\u003e\n\u003cli\u003eRead Committed 每次 select都生成一个快照读\u003c/li\u003e\n\u003cli\u003eRepeatable Read 开启事务后第一个 select 语句才是快照读的地方\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e核心字段\n\u003cul\u003e\n\u003cli\u003em_ids\u003c/li\u003e\n\u003cli\u003emin_trx_id\u003c/li\u003e\n\u003cli\u003emax_trx_id\u003c/li\u003e\n\u003cli\u003ecreator_trx_id\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003eRC隔离级别下，在事务中每次执行快照读时生成 ReadView\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003cimg style=\"background:wheat\" src=\"https://img.wathan.cn/images/2026/06/cd548aef09e32fd1e14c29fa8745e0e1b60269f31af92b320c83320d44bdedf1.svg\"\u003e\n\u003ch2 id=\"mysql-的主从同步原理\"\u003eMySQL 的主从同步原理\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e核心：二进制日志\n\u003cul\u003e\n\u003cli\u003eBINLOG\n\u003cul\u003e\n\u003cli\u003eDDL\u003c/li\u003e\n\u003cli\u003eDML\n主数据库在事务提交时，变更记录写入 Binlog\n从库 binlog -\u0026gt; 中继日志\n从库 中继日志读取事件 -\u0026gt; 从库数据库\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"你们项目用过分库分表吗\"\u003e你们项目用过分库分表吗？\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e什么时候分库分表\n\u003cul\u003e\n\u003cli\u003e单表数据量达到1000W 或者 20GB\u003c/li\u003e\n\u003cli\u003e优化解决不了性能问题\u003c/li\u003e\n\u003cli\u003eIO、CPU瓶颈\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e如何拆分\n\u003cul\u003e\n\u003cli\u003e垂直拆分\n\u003cul\u003e\n\u003cli\u003e垂直分库\n\u003cul\u003e\n\u003cli\u003e不同表拆分到不同库\u003c/li\u003e\n\u003cli\u003e适用于微服务\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e垂直分表\n\u003cul\u003e\n\u003cli\u003e不常用字段单独放在一张表\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e水平拆分\n\u003cul\u003e\n\u003cli\u003e水平分库\n\u003cul\u003e\n\u003cli\u003e一个库的数据拆分到多个库中\u003c/li\u003e\n\u003cli\u003e路由规则\n\u003cul\u003e\n\u003cli\u003e根据id节点取模\u003c/li\u003e\n\u003cli\u003e按id范围路由\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e水平分表\n\u003cul\u003e\n\u003cli\u003e一个表的数据拆分到多个表中\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e新的问题\n\u003cul\u003e\n\u003cli\u003e分布式事务一致性问题\u003c/li\u003e\n\u003cli\u003e跨节点关联查询\u003c/li\u003e\n\u003cli\u003e跨节点分页、排序函数\u003c/li\u003e\n\u003cli\u003e主键去重\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e解决方案\n\u003cul\u003e\n\u003cli\u003e分库分表中间件\n\u003cul\u003e\n\u003cli\u003emycat\u003c/li\u003e\n\u003cli\u003esharding-sphere\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch2 id=\"深入问题\"\u003e深入问题\u003c/h2\u003e\n\u003col\u003e\n\u003cli\u003e为什么 InnoDB 主键建议自增？\u003c/li\u003e\n\u003cli\u003e为什么非自增主键会导致页分裂？\u003c/li\u003e\n\u003cli\u003e为什么 MyISAM 和 InnoDB 索引结构不同？\u003c/li\u003e\n\u003cli\u003e为什么 B+树适合磁盘而红黑树不适合？\u003c/li\u003e\n\u003c/ol\u003e","title":"MySQL 面试题整理"},{"content":"","permalink":"https://lv-blog.pages.dev/posts/programming/jeecg-boot/","summary":"","title":"JeecgBoot有多厉害？我的看法是"},{"content":"没学，啥也不会， 😁 那就微笑面对\n学了，但是不会， 😣 直接系统崩溃\n没学，但是还会， 😎 别问问就社会\n学了也会，但题目就做不对， 🤡 大学四年白废，有点浪费，还累\n啥也不会，但题目就做全对， 👽 必有科技作祟，处处收费，很贵\n学习就是很累，吃亏，还挺遭罪， 😋 但是爸妈都没责备，还会安慰，挥挥，西瓜剁碎\n","permalink":"https://lv-blog.pages.dev/posts/essay/hui-bu-hui/","summary":"\u003cp\u003e没学，啥也不会，\n😁 那就微笑面对\u003c/p\u003e\n\u003cp\u003e学了，但是不会，\n😣 直接系统崩溃\u003c/p\u003e\n\u003cp\u003e没学，但是还会，\n😎 别问问就社会\u003c/p\u003e\n\u003cp\u003e学了也会，但题目就做不对，\n🤡 大学四年白废，有点浪费，还累\u003c/p\u003e\n\u003cp\u003e啥也不会，但题目就做全对，\n👽 必有科技作祟，处处收费，很贵\u003c/p\u003e\n\u003cp\u003e学习就是很累，吃亏，还挺遭罪，\n😋 但是爸妈都没责备，还会安慰，挥挥，西瓜剁碎\u003c/p\u003e","title":"学不会"},{"content":"Redis 的 LFU（Least Frequently Used，最不频繁使用）淘汰策略，是在 LRU 基础上做的“升级版”近似算法。它复用了对象头上同一个 24 位的 lru 字段，通过巧妙的编码和对数计数，用极小的内存代价，实现了对访问频率的近似跟踪。\n下面从存储结构、计数器增减、淘汰决策到参数配置，逐一细究。\n1. 24 位字段的位划分 每个 Redis 对象都有一个 lru 属性（24 位），在 LFU 模式下，它不再存放秒级时间戳，而是被拆成两段：\n高 16 位：最后衰减时间（Last Decay Time，单位：分钟） 低 8 位：对数访问计数器（Logarithmic Counter，范围 0–255） 高 16 位：存储的是 (server.unixtime / 60) \u0026amp; 0xFFFF，即当前分钟时间戳的低 16 位。最大表示约 45 天，足够覆盖淘汰场景，即使回绕，只要间隔不超过 45 天就可以正确计算差值。 低 8 位：是一个 0–255 的频率计数器，但它不是访问次数的直接累加，而是经过对数平滑处理的“近似频率”。 当键被访问时，Redis 会调用 updateLFU()，先根据已流逝的时间衰减计数器，再概率性地递增计数器，最后把新的分钟时间戳和计数器重新编码写回 lru。\n2. 计数器递增：对数增长 为了让 8 位计数器（0–255）既能表示低频也能区分高频，同时不让热门键快速打满，Redis 采用概率递增。\n递增公式（源码级）：\ndouble p = 1.0 / ((counter - LFU_INIT_VAL) * server.lfu_log_factor + 1); if ((random() \u0026amp; 0xFFFF) \u0026lt; p * 0xFFFF) counter++; LFU_INIT_VAL 默认为 5，新键的计数器初始值就是 5。 lfu_log_factor 是可配置的对数因子，默认 10。 随着 counter 增大，p 会越来越小，递增越来越难。 举例（factor = 10 时，典型访问次数与计数器值）：\n访问次数 近似计数器值 10 ~10 100 ~18 1000 ~27 10000 ~36 100000 ~46 1000000 ~55 1 千万 ~63 1 亿 ~73 10 亿 ~82 \u0026hellip; \u0026hellip; 理论最大值 255（极难达到） 可以看到，计数器初期增长较快，后期极其平缓，这样既能让新键快速摆脱“初始低分”，又能让极度热点的键在 8 位空间内被有效区分。\n3. 计数器衰减：随时间线性递减 LFU 的核心是“频率”，但长时间不用的键频率应该降下去。衰减逻辑发生在每次访问键以及淘汰评估时。\n衰减计算：\n// 当前分钟时间戳（16 位） now = LFUGetTimeInMinutes(); // (server.unixtime/60) \u0026amp; 65535 ldt = o-\u0026gt;lru \u0026gt;\u0026gt; 8; // 上次衰减时间 time_diff = now - ldt; // 无符号差值（自动处理回绕） num_periods = time_diff / server.lfu_decay_time; // 衰减周期数 counter = counter - num_periods; // 若负数则置为 0 lfu_decay_time 配置项，默认 1（分钟）。 意思就是：每过 1 分钟，计数器减 1。 如果 lfu_decay_time = 2，则每 2 分钟才减 1，衰减变慢。 因为这个衰减是“一次性追回历史流逝”，例如一个键的计数器现在是 20，最后访问时间是 10 分钟前，decay_time = 1，那么它一被访问（或淘汰评估时），计数器会直接减掉 10，变成 10。长期不碰的键计数会降至 0，非常容易被淘汰。\n4. 新键的保护：初始值 新创建的键不是从 0 开始，而是直接赋予初始计数器 LFU_INIT_VAL（默认 5）。\n这样做的好处是：避免新键一出生还没积累足够访问，就在抽样淘汰中被“冤杀”。\n初始化时的字段编码：\no-\u0026gt;lru = (LFUGetTimeInMinutes() \u0026lt;\u0026lt; 8) | LFU_INIT_VAL; 5. 淘汰决策：抽样近似 LFU Redis 不会维护全局频率排序（代价太大），而是采用抽样近似：\n每次需要淘汰时，从数据库中随机抽取 maxmemory-samples 个键（默认 5）。 对每个样本键，调用 LFUDecrAndReturn(o) 获取衰减后的最新计数器值。 将样本按计数器从小到大排序，放入淘汰候选池。 最终从池中淘汰计数器最小的键。 因此，即使一个键的原始计数器很高，但只要它很久没被访问，衰减后的值会急剧缩小，在淘汰时依然会被优先移除。这正是“频率 + 时间”的综合效果。\n6. 关键配置参数总结 配置项 默认值 含义 maxmemory-policy 无（需主动设置） 设置为 volatile-lfu（仅对设置了过期时间的键）或 allkeys-lfu（所有键） lfu-log-factor 10 对数递增因子，越大增长越慢，计数器可区分的高频范围越广 lfu-decay-time 1（分钟） 衰减周期，每经过这个时间计数器减 1 maxmemory-samples 5 淘汰时的样本数，越大越接近全局 LFU，但 CPU 开销也越大 调优方向：\n如果访问频率差异巨大，可适当加大 lfu-log-factor（如 20），让 8 位计数器能区分更高频次的键。 如果希望冷数据老化更快，减小 lfu-decay-time（甚至 0，但 0 意味着每访问一次就衰减到几乎 0，不推荐）；反之，想让热数据“保温”更久，可增大该值。 提高 maxmemory-samples 可让淘汰更准确，建议不超过 10 以防明显性能下降。 7. 注意事项与局限 回绕问题：16 位分钟时间戳约 45 天回绕一次。只要键的闲置时间小于 45 天（这在淘汰场景下几乎总是成立），无符号差值计算就是正确的。 短时间内大量写入：新键初始值 5，若短时间涌入大量新键，它们可能因采样而互相淘汰，但因其初始值相同，又会退化为近似随机淘汰，可通过适当提高初始值（需修改源码）或结合其他策略缓解。 计数器溢出：由于对数增长极慢，8 位几乎不可能打满，即使面对每秒百万次请求的热键，也需要天文数字的访问才会达到 255，实际生产无虞。 版本差异：LFU 自 Redis 4.0 引入，机制至今保持稳定，上述细节适用于 4.0 至 7.x/8.x 等主流版本。 总结起来，Redis 的 LFU 通过对数计数 + 分钟级衰减 + 抽样淘汰，在常数的内存和 CPU 开销下，巧妙实现了近似频率淘汰，非常适合存在稳定热点、且不希望周期性扫描键被误淘汰的场景。\n","permalink":"https://lv-blog.pages.dev/posts/programming/redis/redis-memory-eviction-policy-lfu/","summary":"\u003cp\u003eRedis 的 LFU（Least Frequently Used，最不频繁使用）淘汰策略，是在 LRU 基础上做的“升级版”近似算法。它复用了对象头上同一个 24 位的 \u003ccode\u003elru\u003c/code\u003e 字段，通过巧妙的编码和对数计数，用极小的内存代价，实现了对访问频率的近似跟踪。\u003c/p\u003e\n\u003cp\u003e下面从存储结构、计数器增减、淘汰决策到参数配置，逐一细究。\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"1-24-位字段的位划分\"\u003e1. 24 位字段的位划分\u003c/h3\u003e\n\u003cp\u003e每个 Redis 对象都有一个 \u003ccode\u003elru\u003c/code\u003e 属性（24 位），在 LFU 模式下，它不再存放秒级时间戳，而是被拆成两段：\u003c/p\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e 高 16 位：最后衰减时间（Last Decay Time，单位：分钟）\n 低  8 位：对数访问计数器（Logarithmic Counter，范围 0–255）\n\u003c/code\u003e\u003c/pre\u003e\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e高 16 位\u003c/strong\u003e：存储的是 \u003ccode\u003e(server.unixtime / 60) \u0026amp; 0xFFFF\u003c/code\u003e，即当前分钟时间戳的低 16 位。最大表示约 45 天，足够覆盖淘汰场景，即使回绕，只要间隔不超过 45 天就可以正确计算差值。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e低 8 位\u003c/strong\u003e：是一个 0–255 的频率计数器，但它\u003cstrong\u003e不是\u003c/strong\u003e访问次数的直接累加，而是经过对数平滑处理的“近似频率”。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e当键被访问时，Redis 会调用 \u003ccode\u003eupdateLFU()\u003c/code\u003e，先根据已流逝的时间衰减计数器，再概率性地递增计数器，最后把新的分钟时间戳和计数器重新编码写回 \u003ccode\u003elru\u003c/code\u003e。\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"2-计数器递增对数增长\"\u003e2. 计数器递增：对数增长\u003c/h3\u003e\n\u003cp\u003e为了让 8 位计数器（0–255）既能表示低频也能区分高频，同时不让热门键快速打满，Redis 采用\u003cstrong\u003e概率递增\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e递增公式（源码级）：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-c\" data-lang=\"c\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003edouble\u003c/span\u003e p \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1.0\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e/\u003c/span\u003e ((counter \u003cspan style=\"color:#f92672\"\u003e-\u003c/span\u003e LFU_INIT_VAL) \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e server.lfu_log_factor \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e1\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003eif\u003c/span\u003e ((\u003cspan style=\"color:#a6e22e\"\u003erandom\u003c/span\u003e() \u003cspan style=\"color:#f92672\"\u003e\u0026amp;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0xFFFF\u003c/span\u003e) \u003cspan style=\"color:#f92672\"\u003e\u0026lt;\u003c/span\u003e p \u003cspan style=\"color:#f92672\"\u003e*\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e0xFFFF\u003c/span\u003e) counter\u003cspan style=\"color:#f92672\"\u003e++\u003c/span\u003e;\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e\u003ccode\u003eLFU_INIT_VAL\u003c/code\u003e 默认为 5，新键的计数器初始值就是 5。\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003elfu_log_factor\u003c/code\u003e 是可配置的对数因子，\u003cstrong\u003e默认 10\u003c/strong\u003e。\u003c/li\u003e\n\u003cli\u003e随着 \u003ccode\u003ecounter\u003c/code\u003e 增大，\u003ccode\u003ep\u003c/code\u003e 会越来越小，递增越来越难。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e举例（factor = 10 时，典型访问次数与计数器值）：\u003c/p\u003e","title":"Redis LFU内存淘汰策略细究"},{"content":" 本笔记基于考纲核心知识点整理，配合代码示例和记忆口诀，适合冲刺复习。\n一、法律法规与标准化 1.1 著作权 类别 内容 不受保护 政府公文、法律条例、时事新闻 受保护 演讲稿、编写的图书、软件代码 归属时间 软件开发完成之日起自动产生 保护期限 50年（署名权、修改权、完整权永久保护） 著作权归属规则：\n谁开发归谁 员工利用公司资源开发 → 归公司 职务作品无合同 → 归企业法人 改编作品 → 著作权归改编人 ⚠️ 处理过程（算法逻辑）不属于软件著作权保护对象，只保护代码表达形式。\n受时间限制的权利（50年）： 发表权、发行权、展览权、复制权\n永久保护的权利： 署名权、修改权、保护作品完整权\n1.2 其他知识产权 商标权：注册完成后才享有；保护对象为软件注册商标 专利权：专利注册完成后才享有 二、软件工程 2.1 软件开发模型对比 模型 适用场景 核心特征 瀑布模型 需求明确固定 阶段严格顺序，不可逆 增量模型 需求部分明确，需快速交付核心 分批次交付模块 原型模型 需求模糊，用户难以描述 先做原型再开发目标软件 螺旋模型 大型复杂高风险项目 每轮增加风险评估环节 喷泉模型 面向对象开发 无严格阶段划分，阶段可交叉迭代 V模型 可靠性要求高 开发与测试一一对应 W模型 质量要求高 开发与测试同步进行 瀑布模型阶段： 需求分析 → 系统设计 → 详细设计 → 编码 → 测试 → 维护 （每个阶段完成后才进入下一阶段） 螺旋模型四象限： ① 制定计划 ② 风险分析 ③ 实施工程 ④ 客户评估 2.2 敏捷开发方法对比 方法 核心理念 关键特征 XP（极限编程） 把传统开发做到极致精简 结对编程、测试先行、持续集成 SCRUM 短周期冲刺 三会议（站会/计划/回顾），需求按商业价值排序 水晶开发 以人为本 最轻量灵活，重团队协作，文档少 FDD 特性驱动 五步：建模→功能清单→规划→设计→实现 ASD 适应性开发 猜测→协作→复盘，三个非线性阶段 DSDM 动态系统开发 八条原则，聚焦业务价值按时交付 开放源码 全球协作 高并行排障，代码公开 SCRUM 三大会议： 每日站会、迭代计划会、复盘回顾会\n2.3 基于 RUP 的软件过程 RUP = Rational Unified Process（统一软件开发过程）\n四个阶段： ┌──────────┬──────────┬──────────┬──────────┐ │ 初始阶段 │ 细化阶段 │ 构建阶段 │ 移交阶段 │ │ 项目范围 │ 完善架构 │ 开发实现 │ 测试交付 │ │ 业务模型 │ │ │ 确认 │ └──────────┴──────────┴──────────┴──────────┘ 九个核心工作流：\n6个过程工作流：业务建模、需求、分析与设计、实现、测试、部署 3个支持工作流：配置与变更管理、项目管理、环境 开发方式： 以用例驱动 + 以体系结构为中心 + 迭代增量\n2.4 软件维护类型 类型 触发原因 示例 改正性维护 修复已知 Bug 修复闪退崩溃 适应性维护 外部环境变化 支持鸿蒙系统 完善性维护 新增用户需求 支持第三方登录 预防性维护 主动防止未来问题 限制登录频率防攻击 📊 工作量占比：完善性 \u0026gt; 适应性 \u0026gt; 改正性 \u0026gt; 预防性\n2.5 能力成熟度模型（CMMI） Level 1 初始级 → 全靠人，无流程，英雄救场 Level 2 可重复级 → 有基本流程，旧经验可复用 Level 3 已定义级 → 全组织统一标准化流程 Level 4 已管理级 → 量化管理，数据驱动决策 Level 5 优化级 → 持续改进，主动迭代升级 记忆口诀：初重定管优\n2.6 逆向工程 信息抽象层次（从高到低）：\n层次 内容 类比 领域级 业务知识 设计思想 功能级 程序段功能 功能模块 结构级 结构图、调用图 系统框架 实现级 语法树、符号表、具体代码 源代码 重构：在同一抽象层级转化描述形式 设计恢复：从已有程序抽象出设计信息 再工程：基于逆向成果产生新版本 使用搜索和变换可导出：实现级 和 结构级 2.7 面向服务架构（SOA） SOA 四大核心技术：\nWSDL → 服务描述（这个服务能干什么，接口文档） Web Service Description Language SOAP → 服务通信（怎么打包发请求，XML信封） Simple Object Access Protocol UDDI → 服务注册与发现（去哪找这个服务） Universal Description, Discovery and Integration BPEL → 服务编排（把多个服务串成一个业务流程） Business Process Execution Language 类比记忆：\n先去UDDI黄页找服务 → 看WSDL说明书了解接口 → 用SOAP信封发请求 → BPEL编排整套业务流程\nESB（企业服务总线）： 由中间件技术实现、支撑SOA的基础架构，负责服务路由、协议转换、消息转换。\n2.8 软件测试与开发阶段对应关系 需求分析 ←→ 验收测试（用户验收） 概要设计 ←→ 系统测试（系统集成） 详细设计 ←→ 集成测试（模块集成） 编码阶段 ←→ 单元测试（依据：详细设计） 2.9 净室软件工程 目标：零缺陷，基于函数理论和抽样理论\n核心要素：形式化规约 → 盒结构设计 → 正确性验证 → 增量统计控制 → 统计测试可靠性\n防错不改错，适合高可靠系统（航天、医疗）\n2.10 构件（Component） 构件特性（重点区别于类/对象）：\n不是实例单元，没有唯一标识 没有外部可见状态（可利用容器管理） 一个构件可包含多个类元素 同一环境中只能有一个拷贝 构件分类方法：\n关键字分类法：树状层次结构 刻面分类法：多维度描述（facet），最灵活 超文本组织法：浏览器式搜索 构件三大性质： 独立可部署性、共享性、可组装性\n架构失配：\n构件失配 → 各种设施、模型不匹配 连接子失配 → 交互协议、数据格式、中间传输不匹配 三、系统分析与设计 3.1 UML 图谱 五大模型与对应图 模型 对应图 用例模型 用例图 静态结构模型 类图、对象图 行为动态模型 时序图、协作图、状态图、活动图 构件实现模型 构件图、部署图 UML 关系速查 关联关系 ─────► 普通认识关系（直线箭头） 依赖关系 - - -► 临时使用关系（虚线箭头） 泛化关系 ───▷ 继承关系（空心三角） 聚合关系 ◇──── 整体包含部分，部分可独立（空菱形） 组合关系 ◆──── 生死绑定，部分不能独立（实菱形） 用例图关系 关系 说明 示例 包含（include） A 必须包含 B 登录 include 验证密码 扩展（extend） B 可选扩展 A 登录 被 extend 找回密码 泛化（generalize） 继承关系 微信支付/支付宝 generalize 统一支付 ⚠️ 用例参与者之间的关系**只有继承（泛化）**一种，没有聚合。\n3.2 设计模式 创建型模式 模式 意图 Java 示例 单例 Singleton 全局唯一实例 Spring Bean 默认单例 工厂方法 父类定义接口，子类决定实例化 BeanFactory 抽象工厂 创建一系列相关产品 跨DB方言切换 建造者 Builder 分步构建复杂对象 StringBuilder、Lombok @Builder 原型 Prototype 克隆已有对象 Object.clone() // 建造者模式示例（Lombok @Builder） @Builder public class UserQuery { private String name; private Integer age; private String city; } // 使用 UserQuery query = UserQuery.builder() .name(\u0026#34;张三\u0026#34;) .age(25) .city(\u0026#34;上海\u0026#34;) .build(); // 单例模式（双重检查锁） public class RedisClient { private volatile static RedisClient instance; private RedisClient() {} public static RedisClient getInstance() { if (instance == null) { synchronized (RedisClient.class) { if (instance == null) { instance = new RedisClient(); } } } return instance; } } 结构型模式 模式 意图 Java 示例 适配器 Adapter 接口转换 InputStreamReader 装饰器 Decorator 动态添加功能 BufferedInputStream 代理 Proxy 控制访问 Spring AOP、MyBatis Mapper 外观 Facade 简化复杂子系统 SLF4J 日志门面 桥接 Bridge 抽象与实现分离 JDBC Driver 组合 Composite 树形结构统一处理 菜单树、文件目录 享元 Flyweight 共享大量相似对象 Integer 缓存池 -128~127 // 代理模式（Spring AOP 本质） @Aspect @Component public class LogAspect { @Around(\u0026#34;@annotation(Log)\u0026#34;) public Object around(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); Object result = pjp.proceed(); log.info(\u0026#34;耗时: {}ms\u0026#34;, System.currentTimeMillis() - start); return result; } } // 装饰器模式示例 InputStream is = new FileInputStream(\u0026#34;data.txt\u0026#34;); InputStream bis = new BufferedInputStream(is); // 添加缓冲功能 InputStream gis = new GZIPInputStream(bis); // 添加解压功能 行为型模式 模式 意图 Java 示例 策略 Strategy 算法族可互换 Comparator、支付方式切换 观察者 Observer 事件通知 EventListener、Kafka Consumer 责任链 Chain 请求沿链传递 Spring Security Filter Chain 模板方法 定义算法骨架 AbstractList、JdbcTemplate 命令 Command 请求封装为对象 撤销/重做、任务队列 状态 State 状态驱动行为变化 订单状态机 迭代器 Iterator 顺序遍历集合 Iterator\u0026lt;T\u0026gt; 访问者 Visitor 不修改类添加操作 AST 遍历、编译器 中介者 Mediator 集中对象交互 MQ、EventBus 备忘录 Memento 保存恢复状态 游戏存档、撤销操作 // 策略模式示例（支付方式） public interface PayStrategy { void pay(BigDecimal amount); } @Component(\u0026#34;alipay\u0026#34;) public class AliPayStrategy implements PayStrategy { public void pay(BigDecimal amount) { /* 支付宝支付逻辑 */ } } @Component(\u0026#34;wechat\u0026#34;) public class WechatPayStrategy implements PayStrategy { public void pay(BigDecimal amount) { /* 微信支付逻辑 */ } } // 责任链模式（Spring Security 过滤链） public class AuthFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws ServletException, IOException { // 前置处理 verifyToken(req); // 传递给下一个过滤器 chain.doFilter(req, res); } } 3.3 面向对象设计七大原则 原则 核心 记忆 单一职责 SRP 一个类只负责一件事 一心不二用 开闭原则 OCP 对扩展开放，对修改封闭 加新功能不改老代码 里氏替换 LSP 子类可完全替换父类 子类不破坏父类契约 依赖倒置 DIP 依赖抽象而非具体实现 面向接口编程 接口隔离 ISP 多个专用接口优于一个通用接口 不强迫实现不需要的方法 合成复用原则 优先使用组合/聚合而非继承 组合优于继承 迪米特法则 LoD 只与直接朋友通信 最少知识原则 3.4 耦合与内聚（从低到高排序） 耦合（低→高）：\n非直接耦合 → 数据耦合 → 标记耦合 → 控制耦合 → 外部耦合 → 公共耦合 → 内容耦合 口诀：飞（非）鼠（数）标（标）恐（控）外（外）公（公）内（内） 内聚（低→高）：\n偶然内聚 → 逻辑内聚 → 时间内聚 → 过程内聚 → 通信内聚 → 顺序内聚 → 功能内聚 口诀：欧（偶）罗（逻）驶（时）过（过）通（通）顺（顺）宫（功） 设计目标：高内聚、低耦合\n3.5 软件架构 4+1 视图 场景视图（用例视图） ↑ ┌─────────────────────┐ │ 逻辑视图 │ 开发视图 │ │ (功能/类图) │(代码分包) │ ├────────────┼─────────┤ │ 处理视图 │ 物理视图 │ │(运行时进程) │(部署架构) │ └─────────────────────┘ 视图 关注点 使用图 场景视图 为谁做（需求验证） 用例图 逻辑视图 做什么（功能分解） 类图、对象图 开发视图 如何组织（代码结构） 包图、组件图 处理视图 什么时候做（并发运行） 时序图、活动图 物理视图 在哪做（硬件部署） 部署图 四、系统架构设计 4.1 架构风格总览（22种） 数据流风格 风格 特征 典型场景 批处理 成批处理，无人工干预，以整体传递数据 银行对账、数据ETL 管道-过滤器 数据流式传输，每个过滤器独立处理 Unix管道、编译器 # 管道-过滤器 Unix 典型示例 cat access.log | grep \u0026#34;ERROR\u0026#34; | awk \u0026#39;{print $4}\u0026#39; | sort | uniq -c # 数据源 过滤器1 过滤器2 过滤器3 过滤器4 调用返回风格 风格 特征 主程序/子程序 栈式调用，层层返回 面向对象 封装属性和方法 层次化架构 按抽象级别分层，上层调用下层 独立构件风格（事件/消息） 风格 特征 示例 事件驱动（隐式调用） 发布事件，订阅者响应 Spring Event、Kafka 进程通信 OS提供IPC机制 管道、共享内存、消息队列 虚拟机风格 风格 特征 示例 解释器 动态解析执行，灵活但效率低 JVM、Python解释器 规则/专家系统 规则集+推理引擎 Drools规则引擎 仓库风格（以数据为中心） 风格 特征 数据库系统 中央数据库作为核心 黑板风格 共享工作内存，适合语音识别、AI推理 超文本系统 HTML+URL的网络结构 闭环控制风格 过程控制风格：传感器采集→控制器计算→执行器响应，注意闭环特征。\n传感器(Sensor) → 控制器(Controller) → 执行器(Actuator) ↑__________________反馈______________________| 4.2 质量属性与架构评估 六大质量属性 运行期质量属性：\n属性 关注点 度量方式 常见战术 性能 响应时间、吞吐量 TPS、RT 缓存、异步、负载均衡 可用性 出事后多久恢复 MTTR 主动冗余、心跳检测、选举 可靠性 多久不出故障 MTBF、MTTF 冗余、降级、熔断 安全性 抗攻击能力 漏洞数 认证授权、加密、审计 MTTR = Mean Time To Repair（平均修复时间）→ 越小越好 MTTF = Mean Time To Failure（平均失效前时间）→ 越大越好 MTBF = Mean Time Between Failure（平均无故障时间）→ 越大越好 MTBF = MTTF + MTTR 开发期质量属性： 可维护性、可扩展性、可测试性、可重用性、可移植性、可修改性、互操作性\n可用性战术 故障检测：心跳（Heartbeat）、Ping-Echo、异常监控 故障恢复：主动冗余（热备）、被动冗余（冷备）、选举（Raft/Paxos） 故障预防：事务、进程监控、预测模型 可修改性战术 中间件解耦 接口与实现分离 抽象 信息隐藏 ⚠️ 可变性不是可修改性考虑的内容\n4.3 架构评估方法 方法 全称 特点 ATAM Architecture Tradeoff Analysis Method 架构权衡分析，开发前评估质量属性折中，最常用 SAAM Software Architecture Analysis Method 基础场景分析，关注非功能需求变化 SASAM — 静态评估方法 SAABNet — 动态分析方法 ATAM 关注四大质量属性：性能、安全性、可用性、可修改性\n质量属性六要素（场景描述模板）：\n刺激源（谁触发）→ 刺激（发生什么）→ 环境（什么状态下） → 制品（影响哪部分）→ 响应（系统怎么做）→ 响应度量（达标标准） 架构评估三个概念：\n架构风险：潜在问题的架构决策所带来的隐患 敏感点：为实现某质量属性，构件所具有的特性 权衡点：影响多个质量属性的特性（多个属性的敏感点） 4.4 ABSD（基于架构的软件开发） 驱动因素： 商业需求 + 质量属性 + 功能需求\n开发过程： 需求 → 设计 → 文档化 → 复审 → 实现 → 演化\n顶层概念架构分解为概念子系统，最终产生软件构件和类\n4.5 DSSA（特定领域软件架构） 三类角色：\n领域分析者 → 产生领域模型 领域设计者 → 开发DSSA，获得架构 领域实现者 → DSSA到具体实现 三个参考： 参考模型 + 参考需求 + 参考架构\n4.6 大数据架构：Lambda vs Kappa 对比维度 Lambda 架构 Kappa 架构 链路 批处理 + 实时双链路 仅实时单链路 计算引擎 Spark(批) + Flink/Storm(流) 仅 Flink/Kafka Streams 数据存储 HDFS(批) + HBase/Redis(实时) Kafka + ClickHouse/Doris 口径一致性 难保证，双链路可能不一致 天然一致 维护成本 高（维护两套代码） 低 数据延迟 批处理延迟高 近实时 适用场景 超大批量、强精准、传统数仓 实时大屏、风控、实时数仓 Lambda 架构： 原始数据 → ┬─ 批处理层(Spark/MapReduce) → 批次视图 ─┐ │ ├→ 查询层(Hive/Impala) └─ 速度层(Flink/Spark Streaming) → 实时视图 ┘ Kappa 架构： 原始数据 → Kafka(消息队列) → Flink(流计算) → ClickHouse/Doris(查询) 4.7 微服务架构 vs 单体架构 维度 单体架构 微服务架构 代码结构 一个代码库 多个独立服务仓库 数据库 共享一个DB 每服务独立DB 部署发布 整体重新部署 独立部署，互不影响 扩容 整体扩容 按需对单个服务扩容 技术栈 统一 可异构 调用方式 本地方法调用 HTTP/RPC跨网络调用 容错性 一个模块崩溃影响全局 服务隔离，故障不蔓延 适用 小型项目快速上线 大型复杂、高并发系统 Spring Cloud 微服务技术栈：\n注册中心：Nacos / Eureka 配置中心：Nacos / Apollo 网关： Spring Cloud Gateway 服务调用：OpenFeign 熔断限流：Sentinel / Resilience4j 消息队列：Kafka / RabbitMQ 分布式事务：Seata 4.8 云计算服务模型 SaaS（Software as a Service） → 最上层，直接用软件 PaaS（Platform as a Service） → 中间层，提供开发平台 IaaS（Infrastructure as a Service）→ 最底层，提供硬件资源 五、数据库系统 5.1 范式 范式 要求 解决问题 1NF 每列原子化，不可再分 列拆分 2NF 满足1NF + 消除部分函数依赖 非主属性完全依赖主键 3NF 满足2NF + 消除传递依赖 A→B→C 改为 A→B, A→C BCNF 满足3NF + 每个决定因素都是候选键 更严格的3NF 4NF 满足BCNF + 消除多值依赖 多值依赖 部分函数依赖示例（违反2NF）： 主键：(学号, 课程号) 问题：学生姓名 只依赖 学号（部分依赖） 解决：拆表 → 学生表(学号,姓名) + 选课表(学号,课程号,成绩) 传递依赖示例（违反3NF）： 学号 → 系名 → 系主任（传递依赖） 解决：拆表 → 学生表(学号,系名) + 系表(系名,系主任) 5.2 关系代数 操作 符号 说明 笛卡尔积 × 硬凑，行数相乘 自然连接 ⋈ 按同名列连接，消除重复列 投影 π 保留哪些列（SELECT 列） 选择 σ 过滤哪些行（WHERE 条件） 并 ∪ 两表合并去重 差 − A中有但B中没有的 交 ∩ 两表共有的 5.3 Armstrong 公理系统 三大基本公理：\n自反律：若 Y⊆X，则 X→Y 增广律：若 X→Y，则 XZ→YZ 传递律：若 X→Y，Y→Z，则 X→Z 三大常用推论：\n合并规则：X→Y, X→Z ⟹ X→YZ 分解规则：X→YZ ⟹ X→Y, X→Z 伪传递规则：X→Y, WY→Z ⟹ WX→Z 5.4 分布式数据库 四种透明性（从高到低）：\n分片透明 → 不知道数据如何分片（最高层透明） 复制透明 → 不知道数据被复制到哪些节点 位置透明 → 不知道数据在哪个节点 逻辑透明 → 不知道底层数据模型（最低层透明） 两阶段提交（2PC）：\nPhase 1（准备/表决阶段）： 协调者 → 所有参与者：\u0026#34;准备好了吗？\u0026#34; 参与者 → 协调者：\u0026#34;Yes/No\u0026#34; Phase 2（执行/提交阶段）： 全Yes → 协调者 → 所有参与者：\u0026#34;COMMIT\u0026#34; 有No → 协调者 → 所有参与者：\u0026#34;ROLLBACK\u0026#34; 分布式数据库概念模式层次：\n全局外模式（顾客视角） ↓ 全局概念模式（总部总账） ↓ 分片模式（拆账规则） ↓ 分布模式（分片存放位置） 5.5 数据库三级模式结构 外模式（用户视图/View） ← 用户看到的数据视图 模式（概念模式/表结构） ← 全局逻辑结构 内模式（物理存储/索引） ← 数据的存储方式 5.6 数据仓库特点 与普通数据库的区别：\n面向主题（而非面向事务） 集成性（多源数据整合） 非易失性（只增不改，历史数据） 时变性（记录时间快照） 六、操作系统 6.1 进程管理 进程三态模型：\n就绪态 ──(调度/分配CPU)──→ 运行态 ↑ │ └──(时间片到/高优先级抢占)──┘ ↑ ↓ └──(I/O完成)── 阻塞/等待态 ──(I/O请求)──┘ PCB（进程控制块）组织方式：\n顺序方式（线性表） 链接方式（链表） 索引方式（索引表） 哈希方式 进程 vs 线程：\n进程：资源分配和管理的最小单位 线程：进程的基本执行单元（CPU调度的最小单位） 6.2 死锁 四个必要条件（必须同时满足才死锁）：\n条件 说明 能否破坏 互斥 资源同时只能一个进程使用 ❌ 不可破坏（资源本质） 请求与保持 持有资源的同时请求新资源 ✅ 可破坏（一次性申请所有） 不可剥夺 资源不能被强制取走 ✅ 可破坏（允许抢占） 循环等待 进程形成环状等待链 ✅ 可破坏（资源编号排序） 银行家算法： 预判分配后系统是否还处于安全状态，如果不安全则拒绝分配。\n6.3 磁盘调度 物理寻址三要素： 磁头号（盘面）→ 柱面号（磁道）→ 扇区号（位置） 访问时间 = 寻道时间 + 旋转延迟 + 数据传输时间 常见磁盘调度算法： FCFS（先来先服务） → 公平但效率低 SSTF（最短寻道时间） → 可能饿死外圈磁道 SCAN（扫描/电梯） → 来回扫描，更均匀 C-SCAN（循环扫描） → 单向扫描，更公平 6.4 嵌入式系统 实时操作系统（RTOS）特点：\n任务调度器：抢占式调度 强实时调度算法：Rate Monotonic Scheduling（RMS） 任务周期越短 → 优先级越高 低功耗设计策略：\n编译优化技术 软硬协同设计 算法优化（减少计算量） 七、信息安全 7.1 加密算法 对称加密 算法 密钥长度 特点 DES 56 位 已不安全 3DES 112 位（56×2） DES的增强版 AES 128/192/256 位 当前标准，安全高效 非对称加密 RSA 原理： 公钥加密 → 私钥解密（实现数据加密传输） 私钥签名 → 公钥验证（实现数字签名） 完整的数字签名流程： 发送方：原文 → Hash → 消息摘要 → 用私钥加密 → 数字签名 接收方：收到(原文+数字签名) → 用公钥解密签名得摘要1 → 对原文Hash得摘要2 → 摘要1==摘要2 则验证通过 消息摘要的作用：防止篡改\n对摘要加密的作用：防止抵赖（数字签名）\n7.2 第三方认证 协议 特点 适用 Kerberos 对称密钥加密，KDC分发密钥，时间戳防重放 企业内网/局域网 PKI/CA 公钥基础设施，CA颁发数字证书 互联网HTTPS Kerberos 防重放攻击的机制：时间戳\n7.3 常见网络攻击 攻击类型 原理 SYN Flooding 利用TCP三次握手，伪造源IP发大量SYN，耗尽半连接资源 Ping of Death 发送超大ICMP包导致缓冲区溢出 Teardrop 发送重叠偏移的分片包导致崩溃 Land 源IP=目的IP的SYN包，导致死循环 业务流分析 监听流量分析通信模式（即使加密也危险） 7.4 信息安全五个等级 Level 1 用户自主保护级 Level 2 系统审计保护级 Level 3 安全标记保护级 Level 4 结构化保护级 Level 5 访问验证保护级（最高） 7.5 灾难恢复最高级别 零数据丢失（RPO=0） 自动系统故障切换（RTO≈0） 八、项目管理 8.1 项目时间管理 PERT 期望时间公式： $$T_e = \\frac{T_{optimistic} + 4 \\times T_{mostLikely} + T_{pessimistic}}{6}$$甘特图 vs PERT图：\n甘特图：展示任务时间安排和进度，直观但不显示依赖关系 PERT图：展示任务依赖和关键路径，可计算最早/最晚完成时间 8.2 WBS（工作分解结构） 项目 ├── 阶段1 │ ├── 工作包1.1 │ └── 工作包1.2 ├── 阶段2 │ ├── 工作包2.1 │ └── 工作包2.2 └── 阶段3 活动定义使用工具：WBS\n8.3 配置管理 配置项的三种状态：\n草稿 → 正式发布 → 正在修改 配置管理四大活动：\n版本控制 变更管理 配置状态管理 访问控制与安全控制 8.4 需求管理 变更控制委员会：CCB（Change Control Board）\n需求变更管理流程：\n1. 问题分析和变更描述 2. 变更分析和成本计算 3. 变更实现 需求管理三大基本活动：\n变更控制 版本控制 需求跟踪 附录：高频考点速记 核心口诀汇总 CMMI 五级：初重定管优（初始、可重复、已定义、已管理、优化） 耦合低→高：飞鼠标恐外公内 （非直接、数据、标记、控制、外部、公共、内容） 内聚低→高：欧罗驶过通顺宫 （偶然、逻辑、时间、过程、通信、顺序、功能） 范式口诀： 1NF=原子，2NF=完全依赖，3NF=无传递，4NF=无多值 架构风格选择指南 需求特征 推荐架构风格 数据流处理、编译器 管道-过滤器 大数据批处理 批处理风格 事件通知、解耦 事件驱动 分层系统、MVC 分层架构 脚本解析、规则引擎 解释器/规则系统 黑板（AI推理、语音识别） 黑板风格 网页系统 B/S、REST 企业系统集成 SOA/ESB 云原生弹性 微服务 嵌入式控制 闭环控制/过程控制 Java 后端关联考点 考点 Java 实现 单例模式 Spring Bean（scope=singleton） 工厂模式 BeanFactory、ApplicationContext 代理模式 Spring AOP（JDK动态代理/CGLIB） 观察者模式 Spring ApplicationEvent 责任链模式 Spring Security FilterChain 策略模式 Comparator、支付策略 模板方法 JdbcTemplate、RestTemplate SOA/ESB Spring Integration、Apache Camel 微服务 Spring Cloud Alibaba 消息队列 Kafka（事件驱动架构） 缓存 Redis（仓库风格的中央数据存储） ","permalink":"https://lv-blog.pages.dev/posts/programming/ruan-kao/","summary":"\u003cblockquote\u003e\n\u003cp\u003e本笔记基于考纲核心知识点整理，配合代码示例和记忆口诀，适合冲刺复习。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"一法律法规与标准化\"\u003e一、法律法规与标准化\u003c/h2\u003e\n\u003ch3 id=\"11-著作权\"\u003e1.1 著作权\u003c/h3\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e类别\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e内容\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e不受保护\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e政府公文、法律条例、时事新闻\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e受保护\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e演讲稿、编写的图书、软件代码\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e归属时间\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e软件开发完成之日起自动产生\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e保护期限\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e50年（署名权、修改权、完整权永久保护）\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cstrong\u003e著作权归属规则：\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e谁开发归谁\u003c/li\u003e\n\u003cli\u003e员工利用公司资源开发 → 归\u003cstrong\u003e公司\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e职务作品无合同 → 归\u003cstrong\u003e企业法人\u003c/strong\u003e\u003c/li\u003e\n\u003cli\u003e改编作品 → 著作权归\u003cstrong\u003e改编人\u003c/strong\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003cblockquote\u003e\n\u003cp\u003e⚠️ \u003cstrong\u003e处理过程（算法逻辑）不属于软件著作权保护对象\u003c/strong\u003e，只保护代码表达形式。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003cp\u003e\u003cstrong\u003e受时间限制的权利（50年）：\u003c/strong\u003e 发表权、发行权、展览权、复制权\u003cbr\u003e\n\u003cstrong\u003e永久保护的权利：\u003c/strong\u003e 署名权、修改权、保护作品完整权\u003c/p\u003e\n\u003ch3 id=\"12-其他知识产权\"\u003e1.2 其他知识产权\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e商标权\u003c/strong\u003e：注册完成后才享有；保护对象为软件注册商标\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e专利权\u003c/strong\u003e：专利注册完成后才享有\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"二软件工程\"\u003e二、软件工程\u003c/h2\u003e\n\u003ch3 id=\"21-软件开发模型对比\"\u003e2.1 软件开发模型对比\u003c/h3\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e模型\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e适用场景\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e核心特征\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e瀑布模型\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e需求明确固定\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e阶段严格顺序，不可逆\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e增量模型\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e需求部分明确，需快速交付核心\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e分批次交付模块\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e原型模型\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e需求模糊，用户难以描述\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e先做原型再开发目标软件\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e螺旋模型\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e大型复杂高风险项目\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e每轮增加\u003cstrong\u003e风险评估\u003c/strong\u003e环节\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e喷泉模型\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e面向对象开发\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e无严格阶段划分，阶段可交叉迭代\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eV模型\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e可靠性要求高\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e开发与测试一一对应\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eW模型\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e质量要求高\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e开发与测试\u003cstrong\u003e同步进行\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e瀑布模型阶段：\n需求分析 → 系统设计 → 详细设计 → 编码 → 测试 → 维护\n（每个阶段完成后才进入下一阶段）\n\n螺旋模型四象限：\n① 制定计划  ② 风险分析\n③ 实施工程  ④ 客户评估\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"22-敏捷开发方法对比\"\u003e2.2 敏捷开发方法对比\u003c/h3\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e方法\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e核心理念\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e关键特征\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eXP（极限编程）\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e把传统开发做到极致精简\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e结对编程、测试先行、持续集成\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eSCRUM\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e短周期冲刺\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e三会议（站会/计划/回顾），需求按\u003cstrong\u003e商业价值\u003c/strong\u003e排序\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e水晶开发\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e以人为本\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e最轻量灵活，重团队协作，文档少\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eFDD\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e特性驱动\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e五步：建模→功能清单→规划→设计→实现\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eASD\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e适应性开发\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e猜测→协作→复盘，三个非线性阶段\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eDSDM\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e动态系统开发\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e八条原则，聚焦业务价值按时交付\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e开放源码\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e全球协作\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e高并行排障，代码公开\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cstrong\u003eSCRUM 三大会议：\u003c/strong\u003e 每日站会、迭代计划会、复盘回顾会\u003c/p\u003e","title":"软考系统架构师 学习"},{"content":"1. 基础篇 1.1 起源：Lucene Lucene：Apache 开源的 Java 全文搜索引擎类库。 优势：易扩展、高性能（纯 Java，可嵌入）。 缺点：使用复杂，需处理索引创建、查询解析等底层细节，无分布式支持。 Elasticsearch 基于 Lucene 构建，提供分布式、易用的 RESTful API。 1.2 技术栈（ELK） Elasticsearch：存储、搜索和分析引擎。 Logstash：服务器端数据处理管道，采集、转换数据后发送至 ES。 Kibana：可视化平台，制作图表、仪表板，管理 ES。 Beats：轻量型数据采集器，发送到 Logstash 或 ES。 1.3 使用 Docker 安装 Elasticsearch（单节点）：\ndocker run -d --name es \\ -e \u0026#34;ES_JAVA_OPTS=-Xms512m -Xmx512m\u0026#34; \\ -e \u0026#34;discovery.type=single-node\u0026#34; \\ -v es-data:/usr/share/elasticsearch/data \\ -v es-plugins:/usr/share/elasticsearch/plugins \\ --privileged \\ --network testNet \\ -p 9200:9200 -p 9300:9300 \\ elasticsearch:7.12.1 9200：HTTP API 端口 9300：内部节点通信端口 discovery.type=single-node：单节点模式（测试用） Kibana：\ndocker run -d --name kibana \\ -e ELASTICSEARCH_HOSTS=http://es:9200 \\ --network=testNet \\ -p 5601:5601 \\ kibana:7.12.1 1.4 倒排索引（Inverted Index） 正向索引：文档 ID → 文档内容。适合根据 ID 精确查找，但做模糊搜索需遍历所有文档，效率低。 倒排索引： 文档（Document）：ES 中存储的一条 JSON 数据。 词条（Term）：文档经过分词后的最小单元。 结构：词条 → 文档 ID 列表（及位置、频率等）。 优点：快速定位包含某个词条的所有文档，实现高效全文搜索。 1.5 分词器（Analyzer） 将文本拆分为词条（term）的组件，由三部分组成：\nCharacter Filter：预处理（如去除 HTML 标签）。 Tokenizer：按规则分词。 Token Filter：对词条再加工（转小写、去除停用词）。 IK 分词器：最常用的中文分词插件。\n安装：下载对应版本 zip 放到 plugins/ik 目录，重启 ES。 两种模式： ik_smart：最粗粒度拆分。 ik_max_word：最细粒度拆分。 自定义词典：修改 IKAnalyzer.cfg.xml，添加 ext.dic（新词）、stopword.dic（停用词）。 1.6 基础概念 索引（Index）：相同类型文档的集合，类似数据库中的“表”。 文档（Document）：ES 存储的基本单元，序列化为 JSON。 映射（Mapping）：定义文档字段的类型、分词器等，类似数据库中的“Schema”。 Mapping 常用字段类型：\n字符串：text（分词）、keyword（精确匹配，不分词）。 数值：long, integer, short, byte, double, float。 日期：date。 布尔：boolean。 地理：geo_point（经纬度）、geo_shape（区域）。 对象：object（嵌套字段）。 数组：不专门定义，ES 自动支持。 2. 索引库操作（RESTful API） 所有操作通过 HTTP API 发送 JSON 格式数据。\n2.1 创建索引（Create） PUT /索引名 { \u0026#34;settings\u0026#34;: { \u0026#34;number_of_shards\u0026#34;: 3, // 分片数 \u0026#34;number_of_replicas\u0026#34;: 2 // 副本数 }, \u0026#34;mappings\u0026#34;: { \u0026#34;properties\u0026#34;: { \u0026#34;字段名\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;text\u0026#34;, // 字段类型 \u0026#34;analyzer\u0026#34;: \u0026#34;ik_max_word\u0026#34; // 分词器 } } } } 2.2 查看索引（Read） GET /索引名 GET /索引名/_mapping GET /_cat/indices?v # 查看所有索引 2.3 修改索引（Update） 全量修改文档：PUT 请求带全量 JSON 数据，相当于先删除旧文档再创建。 局部更新文档：POST 请求使用 _update API。 POST /索引名/_update/文档ID { \u0026#34;doc\u0026#34;: { \u0026#34;字段\u0026#34;: \u0026#34;新值\u0026#34; } } Mapping 更新限制：字段类型一旦创建，多数不可修改（如 text 改为 keyword），但可增加新字段。若需修改类型，需重建索引。\n2.4 删除索引（Delete） DELETE /索引名 2.5 批量操作（Bulk） 一条请求完成多个文档的增删改。\nPOST /_bulk {\u0026#34;index\u0026#34;:{\u0026#34;_index\u0026#34;:\u0026#34;idx\u0026#34;,\u0026#34;_id\u0026#34;:\u0026#34;1\u0026#34;}} {\u0026#34;title\u0026#34;:\u0026#34;Doc 1\u0026#34;} {\u0026#34;delete\u0026#34;:{\u0026#34;_index\u0026#34;:\u0026#34;idx\u0026#34;,\u0026#34;_id\u0026#34;:\u0026#34;2\u0026#34;}} {\u0026#34;update\u0026#34;:{\u0026#34;_index\u0026#34;:\u0026#34;idx\u0026#34;,\u0026#34;_id\u0026#34;:\u0026#34;3\u0026#34;}} {\u0026#34;doc\u0026#34;:{\u0026#34;title\u0026#34;:\u0026#34;Updated\u0026#34;}} 从数据库表设计 Mapping 的实践：\nMySQL 的 varchar → 需全文搜索用 text，需精确查询/排序用 keyword。 数字类型对应 integer/long/float。 日期类型统一用 date，可指定格式。 涉及经纬度查询用 geo_point。 不需要搜索的字段设置 index: false 节省空间。 3. Java REST 客户端 官方推荐 Java High Level REST Client（7.x 版本），Elasticsearch Java API Client（8.x+ 推荐）。此处以 7.x 高级客户端为例。\n引入依赖：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.elasticsearch.client\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;elasticsearch-rest-high-level-client\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;7.12.1\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 初始化客户端：\nRestHighLevelClient client = new RestHighLevelClient( RestClient.builder(new HttpHost(\u0026#34;localhost\u0026#34;, 9200, \u0026#34;http\u0026#34;)) ); 3.1 索引库操作 // 创建索引 CreateIndexRequest request = new CreateIndexRequest(\u0026#34;my_index\u0026#34;); request.settings(Settings.builder() .put(\u0026#34;index.number_of_shards\u0026#34;, 3) .put(\u0026#34;index.number_of_replicas\u0026#34;, 2)); request.mapping(\u0026#34;{\\\u0026#34;properties\\\u0026#34;:{...}}\u0026#34;, XContentType.JSON); CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT); // 删除索引 DeleteIndexRequest deleteRequest = new DeleteIndexRequest(\u0026#34;my_index\u0026#34;); client.indices().delete(deleteRequest, RequestOptions.DEFAULT); // 存在性检查 GetIndexRequest getRequest = new GetIndexRequest(\u0026#34;my_index\u0026#34;); boolean exists = client.indices().exists(getRequest, RequestOptions.DEFAULT); 3.2 文档操作 // 添加文档 IndexRequest indexReq = new IndexRequest(\u0026#34;my_index\u0026#34;).id(\u0026#34;1\u0026#34;); User user = new User(\u0026#34;张三\u0026#34;, 25); indexReq.source(JSON.toJSONString(user), XContentType.JSON); IndexResponse indexResp = client.index(indexReq, RequestOptions.DEFAULT); // 局部更新 UpdateRequest updateReq = new UpdateRequest(\u0026#34;my_index\u0026#34;, \u0026#34;1\u0026#34;) .doc(\u0026#34;age\u0026#34;, 26); client.update(updateReq, RequestOptions.DEFAULT); // 批量操作 BulkRequest bulk = new BulkRequest(); bulk.add(new IndexRequest(\u0026#34;my_index\u0026#34;).id(\u0026#34;1\u0026#34;).source(...)); bulk.add(new DeleteRequest(\u0026#34;my_index\u0026#34;, \u0026#34;2\u0026#34;)); client.bulk(bulk, RequestOptions.DEFAULT); 3.3 查询操作 构建查询条件，执行搜索。\nSearchRequest searchRequest = new SearchRequest(\u0026#34;my_index\u0026#34;); SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); sourceBuilder.query(QueryBuilders.matchQuery(\u0026#34;title\u0026#34;, \u0026#34;elasticsearch\u0026#34;)); searchRequest.source(sourceBuilder); SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT); SearchHit[] hits = response.getHits().getHits(); for (SearchHit hit : hits) { String json = hit.getSourceAsString(); User user = JSON.parseObject(json, User.class); } 4. DSL 查询语法 DSL（Domain Specific Language）基于 JSON 构建查询，是 ES 最强大的搜索方式。\n4.1 查询分类概览 叶子查询：在特定字段查询具体值。 全文检索（Full Text）：分词后匹配，有相关性算分。 精确查询（Term-Level）：不分词，直接精确匹配词条。 地理查询（Geo）：坐标查询。 复合查询：组合多个叶子或复合查询，修改算分、过滤等。 特殊查询：如 script, exists 等。 4.2 全文检索查询 适用于 text 字段，会分词并计算相关性分数 _score。\nmatch：模糊匹配，分词后 OR 关系。\nGET /index/_search { \u0026#34;query\u0026#34;: { \u0026#34;match\u0026#34;: { \u0026#34;content\u0026#34;: \u0026#34;Elasticsearch 入门\u0026#34; } } } match_phrase：短语匹配，要求分词后顺序一致、连续。\n{ \u0026#34;query\u0026#34;: { \u0026#34;match_phrase\u0026#34;: { \u0026#34;content\u0026#34;: \u0026#34;Elasticsearch 入门\u0026#34; } } } multi_match：多字段匹配。\n{ \u0026#34;query\u0026#34;: { \u0026#34;multi_match\u0026#34;: { \u0026#34;query\u0026#34;: \u0026#34;入门\u0026#34;, \u0026#34;fields\u0026#34;: [\u0026#34;title\u0026#34;, \u0026#34;content\u0026#34;] } } } 4.3 精确查询 适用于 keyword、数字、日期、布尔，不计算相关性分数。\nterm：精确值匹配。\n{ \u0026#34;query\u0026#34;: { \u0026#34;term\u0026#34;: { \u0026#34;status\u0026#34;: \u0026#34;active\u0026#34; } } } range：范围查询。\n{ \u0026#34;query\u0026#34;: { \u0026#34;range\u0026#34;: { \u0026#34;price\u0026#34;: { \u0026#34;gte\u0026#34;: 100, \u0026#34;lte\u0026#34;: 500 } } } } terms：多值精确匹配。\n{ \u0026#34;query\u0026#34;: { \u0026#34;terms\u0026#34;: { \u0026#34;category\u0026#34;: [\u0026#34;科技\u0026#34;, \u0026#34;教育\u0026#34;] } } } 4.4 地理查询（Geo） 需要字段类型为 geo_point。\n// 矩形范围 { \u0026#34;query\u0026#34;: { \u0026#34;geo_bounding_box\u0026#34;: { \u0026#34;location\u0026#34;: { \u0026#34;top_left\u0026#34;: { \u0026#34;lat\u0026#34;: 40, \u0026#34;lon\u0026#34;: 116 }, \u0026#34;bottom_right\u0026#34;: { \u0026#34;lat\u0026#34;: 39, \u0026#34;lon\u0026#34;: 117 } } } } } // 距离范围（圆心半径） { \u0026#34;query\u0026#34;: { \u0026#34;geo_distance\u0026#34;: { \u0026#34;distance\u0026#34;: \u0026#34;10km\u0026#34;, \u0026#34;location\u0026#34;: { \u0026#34;lat\u0026#34;: 39.9, \u0026#34;lon\u0026#34;: 116.4 } } } } 4.5 复合查询（Compound Queries） bool 查询：组合多个条件，包含 must（AND）、should（OR）、must_not（NOT）、filter（过滤，不计算分数）。\n{ \u0026#34;query\u0026#34;: { \u0026#34;bool\u0026#34;: { \u0026#34;must\u0026#34;: [ { \u0026#34;match\u0026#34;: { \u0026#34;title\u0026#34;: \u0026#34;手机\u0026#34; } } ], \u0026#34;filter\u0026#34;: [ { \u0026#34;range\u0026#34;: { \u0026#34;price\u0026#34;: { \u0026#34;gte\u0026#34;: 1000, \u0026#34;lte\u0026#34;: 3000 } } }, { \u0026#34;term\u0026#34;: { \u0026#34;brand\u0026#34;: \u0026#34;华为\u0026#34; } } ], \u0026#34;must_not\u0026#34;: [ { \u0026#34;term\u0026#34;: { \u0026#34;status\u0026#34;: \u0026#34;下架\u0026#34; } } ], \u0026#34;should\u0026#34;: [ { \u0026#34;term\u0026#34;: { \u0026#34;feature\u0026#34;: \u0026#34;5G\u0026#34; } }, { \u0026#34;term\u0026#34;: { \u0026#34;feature\u0026#34;: \u0026#34;快充\u0026#34; } } ], \u0026#34;minimum_should_match\u0026#34;: 1 } } } filter 不参与算分，性能更好，建议首选用于过滤条件。 4.6 排序和分页 排序：\n{ \u0026#34;query\u0026#34;: { \u0026#34;match_all\u0026#34;: {} }, \u0026#34;sort\u0026#34;: [ { \u0026#34;price\u0026#34;: \u0026#34;desc\u0026#34; }, { \u0026#34;_score\u0026#34;: \u0026#34;desc\u0026#34; } ] } 对 text 字段排序需打开 fielddata 或使用 keyword 子字段。\n分页：\n{ \u0026#34;query\u0026#34;: { \u0026#34;match_all\u0026#34;: {} }, \u0026#34;from\u0026#34;: 0, // 起始偏移量 \u0026#34;size\u0026#34;: 10 // 每页文档数 } 深度分页问题：\n随着 from 增大，ES 需要从每个分片取 from+size 条数据在协调节点排序，内存与耗时急剧增加。 解决方案： search_after：利用上一页的最后一条文档的排序值，实时查询下一页，无 from 开销，但不支持跳页。 scroll：生成数据快照，适合遍历所有数据（如导出），但非实时。 限制分页深度（如 max_result_window: 10000）。 4.7 高亮显示（Highlight） { \u0026#34;query\u0026#34;: { \u0026#34;match\u0026#34;: { \u0026#34;content\u0026#34;: \u0026#34;elasticsearch\u0026#34; } }, \u0026#34;highlight\u0026#34;: { \u0026#34;fields\u0026#34;: { \u0026#34;content\u0026#34;: { \u0026#34;pre_tags\u0026#34;: [\u0026#34;\u0026lt;em\u0026gt;\u0026#34;], \u0026#34;post_tags\u0026#34;: [\u0026#34;\u0026lt;/em\u0026gt;\u0026#34;] } } } } 响应中会额外返回 highlight 字段，包含高亮片段。\n4.8 数据聚合（Aggregations） 聚合可从数据中提取统计、分组等分析信息。\n聚合分类：\nBucket（桶聚合）：分组，如 terms, range, date_histogram。 Metric（指标聚合）：计算统计值，如 avg, sum, max, min, stats。 Pipeline（管道聚合）：对其他聚合结果再次计算。 DSL 聚合示例：\n{ \u0026#34;size\u0026#34;: 0, // 不返回文档，只关心聚合结果 \u0026#34;aggs\u0026#34;: { \u0026#34;brand_group\u0026#34;: { \u0026#34;terms\u0026#34;: { \u0026#34;field\u0026#34;: \u0026#34;brand\u0026#34; }, \u0026#34;aggs\u0026#34;: { \u0026#34;avg_price\u0026#34;: { \u0026#34;avg\u0026#34;: { \u0026#34;field\u0026#34;: \u0026#34;price\u0026#34; } } } } } } 含义：按品牌分组，计算每组的平均价格。\nJava 客户端实现聚合：\nSearchRequest searchRequest = new SearchRequest(\u0026#34;items\u0026#34;); SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); sourceBuilder.query(QueryBuilders.matchAllQuery()); // 聚合构建 TermsAggregationBuilder aggregation = AggregationBuilders .terms(\u0026#34;brand_group\u0026#34;) .field(\u0026#34;brand.keyword\u0026#34;) .subAggregation(AggregationBuilders.avg(\u0026#34;avg_price\u0026#34;).field(\u0026#34;price\u0026#34;)); sourceBuilder.aggregation(aggregation); searchRequest.source(sourceBuilder); SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT); Aggregations aggs = response.getAggregations(); Terms brandTerms = aggs.get(\u0026#34;brand_group\u0026#34;); for (Terms.Bucket bucket : brandTerms.getBuckets()) { String brand = bucket.getKeyAsString(); Avg avgPrice = bucket.getAggregations().get(\u0026#34;avg_price\u0026#34;); System.out.println(brand + \u0026#34;:\u0026#34; + avgPrice.getValue()); } 5. ES 核心原理补充 5.1 分片与副本 分片（Shard）：索引被水平拆分为多个分片，每个分片是一个独立的 Lucene 索引。 优势：分布式存储，并行搜索，水平扩展容量和吞吐。 副本（Replica）：每个主分片的拷贝，提供高可用，分担查询压力。 分片数在索引创建后不可修改（可通过 _split/_shrink API 调整）。 副本数可动态调整。 5.2 写入与搜索流程 写入：文档被路由到某个主分片，主分片写入后转发给副本，全部完成才返回确认。 搜索：协调节点将请求转发给相关分片（主或副本），每个分片返回结果，协调节点汇总排序。 5.3 优化建议 避免字段过多、深度聚合。 使用 filter 代替 must 减少评分计算。 合理设计 Mapping，用 keyword 代替 text 做精确搜索。 冷热分离，结合索引生命周期管理（ILM）。 ","permalink":"https://lv-blog.pages.dev/posts/programming/backend/elasticsearch/","summary":"\u003ch2 id=\"1-基础篇\"\u003e1. 基础篇\u003c/h2\u003e\n\u003ch3 id=\"11-起源lucene\"\u003e1.1 起源：Lucene\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eLucene\u003c/strong\u003e：Apache 开源的 Java 全文搜索引擎类库。\u003c/li\u003e\n\u003cli\u003e优势：易扩展、高性能（纯 Java，可嵌入）。\u003c/li\u003e\n\u003cli\u003e缺点：使用复杂，需处理索引创建、查询解析等底层细节，无分布式支持。\u003c/li\u003e\n\u003cli\u003eElasticsearch 基于 Lucene 构建，提供分布式、易用的 RESTful API。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"12-技术栈elk\"\u003e1.2 技术栈（ELK）\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eElasticsearch\u003c/strong\u003e：存储、搜索和分析引擎。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eLogstash\u003c/strong\u003e：服务器端数据处理管道，采集、转换数据后发送至 ES。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eKibana\u003c/strong\u003e：可视化平台，制作图表、仪表板，管理 ES。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eBeats\u003c/strong\u003e：轻量型数据采集器，发送到 Logstash 或 ES。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"13-使用-docker-安装\"\u003e1.3 使用 Docker 安装\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eElasticsearch\u003c/strong\u003e（单节点）：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edocker run -d --name es \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  -e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;ES_JAVA_OPTS=-Xms512m -Xmx512m\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  -e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;discovery.type=single-node\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  -v es-data:/usr/share/elasticsearch/data \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  -v es-plugins:/usr/share/elasticsearch/plugins \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  --privileged \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  --network testNet \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  -p 9200:9200 -p 9300:9300 \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  elasticsearch:7.12.1\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cul\u003e\n\u003cli\u003e9200：HTTP API 端口\u003c/li\u003e\n\u003cli\u003e9300：内部节点通信端口\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003ediscovery.type=single-node\u003c/code\u003e：单节点模式（测试用）\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003eKibana\u003c/strong\u003e：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003edocker run -d --name kibana \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  -e ELASTICSEARCH_HOSTS\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003ehttp://es:9200 \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  --network\u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003etestNet \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  -p 5601:5601 \u003cspan style=\"color:#ae81ff\"\u003e\\\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e  kibana:7.12.1\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"14-倒排索引inverted-index\"\u003e1.4 倒排索引（Inverted Index）\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e正向索引\u003c/strong\u003e：文档 ID → 文档内容。适合根据 ID 精确查找，但做模糊搜索需遍历所有文档，效率低。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e倒排索引\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e文档（Document）\u003c/strong\u003e：ES 中存储的一条 JSON 数据。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e词条（Term）\u003c/strong\u003e：文档经过分词后的最小单元。\u003c/li\u003e\n\u003cli\u003e结构：词条 → 文档 ID 列表（及位置、频率等）。\u003c/li\u003e\n\u003cli\u003e优点：快速定位包含某个词条的所有文档，实现高效全文搜索。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"15-分词器analyzer\"\u003e1.5 分词器（Analyzer）\u003c/h3\u003e\n\u003cp\u003e将文本拆分为词条（term）的组件，由三部分组成：\u003c/p\u003e","title":"ElasticSearch 学习"},{"content":" 面向系统架构师认证，结合 Java 后端视角整理。核心思路：每个服务解决什么痛点、何时选它、和类似服务怎么区分。\n一、存储 Storage 1.1 三种存储类型对比 类型 代表服务 访问单元 典型场景 块存储 EBS, EC2 Instance Store 数据块（Block） 数据库、OS 磁盘 文件存储 EFS, FSx 文件/目录树 多实例共享、NFS/SMB挂载 对象存储 S3 对象（Object + Key） 静态资源、备份、数据湖 块存储 → 像本地硬盘，OS看到的是裸设备，自己格式化挂载 文件存储 → 像 NAS，多台机器可以同时 mount 同一个目录 对象存储 → 像 HTTP PUT/GET 的 Key-Value，无目录概念，靠前缀模拟 1.2 Amazon S3 核心功能速查：\n功能 说明 常见考点 Versioning 同一 Key 保留多个历史版本 开启后才能用 CRR / MFA Delete CRR（跨区域复制） 异步复制到另一 Region 灾备 DR、降低延迟 Transfer Acceleration 通过 CloudFront 边缘节点加速上传 上传到遥远 Region 时使用 Lifecycle Policy 对象按年龄自动迁移存储类 节省成本核心手段 S3 File Gateway 本地 SMB/NFS 映射到 S3 混合云文件迁移 Intelligent-Tiering 自动在 Standard ↔ IA 间切换 访问模式不可预测时使用 S3 存储类选择决策树：\n访问频率高？ → S3 Standard ↓ 否 访问频率低但偶尔需要立即取回？ → S3 Standard-IA ↓ 只需单 AZ 且可接受丢失风险？ → S3 One Zone-IA（比 IA 便宜20%） ↓ 极少访问（档案级）？ → Glacier Instant Retrieval ← 毫秒取回 → Glacier Flexible Retrieval ← 分钟~小时 → Glacier Deep Archive ← 12~48小时，最便宜 S3 CRR 配置要点：\n1. 源 Bucket 和目标 Bucket 都必须开启 Versioning 2. 在源 Bucket 配置 Replication Rule（选目标 Bucket/Region） 3. 创建/指定 IAM Role（授权 S3 读源 + 写目标） 4. 目标 Bucket 可跨账号，存储类可以降级（节省成本） 5. 已有对象不会被复制，只复制规则创建后的新对象 1.3 Amazon EBS vs EFS vs Instance Store 特性 EBS EFS Instance Store 类型 块存储 文件存储（NFS） 块存储（物理本地） 持久化 ✅ 持久 ✅ 持久 ❌ 实例停止即消失 多实例挂载 ❌（默认单挂）Multi-Attach 有限支持 ✅ 同时挂载数千个 跨 AZ ❌ 只能同一 AZ ✅ 跨 AZ/Region ❌ 物理绑定 性能 高（可选 io2 Block Express） 中（NFS 协议开销） 极高（本地 NVMe） 典型场景 数据库、单实例应用 多实例共享、CMS 临时缓存、大数据计算 ⚠️ EBS 孤岛效应：EBS 只能在同一 AZ 内挂载。跨 AZ 迁移需先做 Snapshot，再从 Snapshot 在目标 AZ 创建新 EBS。\n1.4 AWS DataSync vs Storage Gateway 服务 定位 典型用途 DataSync 高速数据迁移/同步工具 一次性迁移 + 定时增量同步；NFS/SMB → S3/EFS/FSx S3 File Gateway 长期混合云存储桥接 本地应用继续用 NFS/SMB，数据透明存到 S3 迁移场景（临时） → DataSync（快、省带宽、自动校验） 持续混合云存储 → Storage Gateway（长期共存） 1.5 Amazon FSx 类型速查 类型 适用场景 协议 FSx for Windows Windows 应用、AD 集成 SMB FSx for Lustre HPC、机器学习、高性能计算 Lustre（可直接接 S3） FSx for NetApp ONTAP 企业级 NAS 迁移上云 NFS/SMB/iSCSI FSx for OpenZFS ZFS 文件系统迁移 NFS 二、数据库 Database 2.1 数据库选型速查 需求 推荐服务 理由 关系型、低流量 RDS（MySQL/PostgreSQL） 标准托管，成本合理 关系型、高性能/高可用 Aurora 性能 5x MySQL，自动多副本 NoSQL、超低延迟 DynamoDB 单位毫秒，完全 Serverless DynamoDB 加速 DAX 微秒级内存缓存，对应用透明 数据仓库 OLAP Redshift 列式存储，PB 级分析查询 内存缓存 ElastiCache Redis / Memcached 托管 2.2 Amazon Aurora 解决了什么痛点 传统 RDS MySQL 痛点 Aurora 解法 ───────────────────────────────────────────── 扩展困难 → 存储自动扩展（最大 128TB） 高可用难（容易挂） → 6 副本跨 3 AZ，1 写 + 最多 15 读副本 高并发支撑差 → Aurora Serverless v2 自动扩缩容 运维成本高 → 全托管，自动 patching/backup 存储利用率低 → Log-structured 存储，物理空间共享 启动慢，弹性差 → Aurora Serverless 秒级冷启动 Aurora vs RDS 选择：\n需要 高可用、大并发、读扩展 → Aurora 普通应用、成本敏感 → RDS 2.3 DynamoDB 核心概念 Table（表） └── Item（行，最大 400KB） └── Attribute（列，动态schema） 主键类型： Partition Key（哈希键） → 决定数据分布在哪个分区 Partition Key + Sort Key → 组合主键，同一分区内按 Sort Key 排序 读写容量模式： Provisioned → 手动设置 RCU/WCU（适合稳定流量，省钱） On-Demand → 按请求计费（适合变化流量，无需预估） DAX（DynamoDB Accelerator）：\n应用 → DAX（内存缓存，微秒响应）→ DynamoDB ↑ 缓存命中直接返回 ↓ Miss 才回源 DynamoDB 适合：读多写少，相同 Query 反复执行 不适合：强一致性读、写密集型场景 三、消息队列 Messaging 3.1 SNS vs SQS vs EventBridge 特性 SNS SQS EventBridge 模型 发布/订阅（Push） 队列（Poll） 事件总线（路由） 消费者 多个订阅者同时收到 一个消费者处理一条消息 多个 Target 并行路由 持久化 ❌（不持久，发即消失） ✅（保留最多 14 天） ❌（事件路由后不存储） 重试 有限 ✅（Visibility Timeout） ✅（可配置重试策略） 消息顺序 ❌ FIFO Queue 支持 ❌ 典型场景 通知广播、触发多个系统 任务队列、削峰填谷 AWS 服务事件响应 3.2 SNS + SQS 扇出模式（Fan-out）⭐ 场景： 一条消息需要同时分发给多个独立的消费者（微服务）\n┌── SQS Queue A ── 微服务A（发邮件） 消息生产者 → SNS Topic ┤ ├── SQS Queue B ── 微服务B（记录日志） └── SQS Queue C ── 微服务C（更新库存） 为什么不直接用 SNS？\nSNS 直接 Push 到服务，若某微服务宕机，消息永久丢失。 加入 SQS 缓冲后，消息积压在队列，服务恢复后继续消费，零丢失。\n配置要点：\n1. 创建 SNS Topic 2. 为每个下游服务创建独立 SQS Queue 3. 每个 SQS Queue 订阅该 SNS Topic 4. 下游服务各自 Poll 自己的 SQS Queue 5. 开启 SQS DLQ（死信队列）处理消费失败消息 3.3 SQS 核心概念 Standard Queue → 至少投递一次（At-Least-Once），近似 FIFO，高吞吐 FIFO Queue → 精确投递一次（Exactly-Once），严格有序，300 TPS 上限 Visibility Timeout： 消息被消费者取走后，对其他消费者不可见的时间窗口 消费者处理完需在超时前 DeleteMessage，否则消息重新可见 DLQ（Dead Letter Queue）死信队列： 消息超过 maxReceiveCount 次仍未被成功处理 → 转移到 DLQ 用途：排查问题，避免毒药消息卡死队列 四、计算 Compute 4.1 EC2 核心 Auto Scaling Group（ASG）工作原理：\n定义：最小/期望/最大实例数 ↓ CloudWatch 监控指标（CPU、请求数等） ↓ 触发 Scaling Policy（扩出 / 缩入） ↓ ALB 自动注册/注销实例 Scaling 策略类型：\n策略 说明 Target Tracking 维持某个指标在目标值（最推荐，如 CPU=60%） Step Scaling 按阶梯规则扩缩（超过70%加2台，超过90%加5台） Scheduled Scaling 定时扩缩（每天早8点扩容，晚10点缩容） Predictive Scaling ML 预测未来流量，提前扩容 4.2 AWS Lambda 核心限制（SAA 常考）：\n最大执行时间：15 分钟 内存：128MB ~ 10,240MB 临时存储（/tmp）：最大 10GB 并发数：默认 1000/Region（可申请提升） 部署包大小：50MB（zip），250MB（解压后） Lambda 适用场景判断：\n✅ 适合 Lambda： - 事件触发型（S3上传、API请求、定时任务） - 执行时间 \u0026lt; 15分钟 - 无状态处理 - 流量变化大（不想管服务器） ❌ 不适合 Lambda： - 长时间运行任务（\u0026gt;15分钟）→ 用 Fargate / EC2 - 需要持久化本地状态 → 用 EC2 + EBS - 高频持续负载（冷启动成本高）→ 用 EC2 + ASG Lambda + API Gateway 典型架构（Serverless）：\n用户请求 → Route 53 → API Gateway ↓ Lambda 函数 ↓ DynamoDB / S3 / RDS 五、网络与连接 Networking 5.1 CloudFront vs Global Accelerator 对比维度 CloudFront（CDN） Global Accelerator 核心能力 缓存内容，减少回源 优化路径，减少网络跳数 协议 HTTP/HTTPS 所有协议（TCP/UDP） 缓存 ✅ 有缓存 ❌ 无缓存，透传 IP 地址 动态（DNS解析） 静态 Anycast IP（2个） 适用场景 静态资源、网站加速 游戏、VoIP、金融交易、全球 API 记忆口诀： CloudFront = 内容缓存（把东西搬到离用户近的地方） Global Accelerator = 网络提速（走 AWS 高速公路而非公网） 5.2 VPC 核心组件 VPC（Virtual Private Cloud） ├── Subnet（子网） │ ├── Public Subnet → 有 Internet Gateway 路由，实例可有公网 IP │ └── Private Subnet → 无直接公网访问，通过 NAT Gateway 出网 ├── Internet Gateway（IGW） → VPC 出公网的门 ├── NAT Gateway → Private 子网实例出公网（单向） ├── Route Table → 控制流量走向 ├── Security Group（SG） → 实例级，有状态防火墙 └── Network ACL（NACL） → 子网级，无状态防火墙 SG vs NACL 关键区别：\n特性 Security Group Network ACL 作用级别 实例（ENI） 子网 状态 有状态（出去的回来自动放行） 无状态（进出都要显式写规则） 规则 只有 Allow Allow + Deny 评估 所有规则取并集 按编号顺序，第一个匹配即生效 ⚠️ NACL 无状态是高频考点：如果只配了入站规则允许 HTTP，但没有配出站规则允许响应端口，流量会被拦截。\n5.3 VPC Endpoints 问题：VPC 内的 EC2 访问 S3，流量默认走公网 → 有安全风险 + 流量费用 解决：VPC Endpoint（内网直通，不过公网） 类型： Gateway Endpoint → S3、DynamoDB（免费，配置在 Route Table） Interface Endpoint → 其他 AWS 服务（收费，部署 ENI 到子网） 5.4 连接方案对比 场景 推荐方案 本地数据中心 ↔ AWS（低延迟，高安全） AWS Direct Connect（专线，物理连接） 本地数据中心 ↔ AWS（加密隧道） Site-to-Site VPN（走公网但加密，成本低） 开发者笔记本 ↔ VPC 内资源 Client VPN 不同 VPC 内网互通 VPC Peering 或 Transit Gateway 多个 VPC / On-Prem 星型互联 Transit Gateway（Hub-Spoke 拓扑） 5.5 Route 53 路由策略 策略 说明 场景 Simple 直接解析，无特殊逻辑 单端点 Weighted 按权重分流（A:70%, B:30%） 灰度发布、A/B测试 Latency 路由到延迟最低的 Region 全球多 Region 部署 Failover 主备切换，健康检查触发 灾备 DR Geolocation 按用户地理位置路由 合规要求（欧洲用户只访问欧洲） Geoproximity 按距离 + 偏置权重路由 精细流量偏移 Multi-Value 返回多个健康 IP 简单客户端负载均衡 六、安全与权限 Security \u0026amp; IAM 6.1 IAM 核心概念 用户（User） → 对应具体的人或程序，有 AK/SK 用户组（Group） → 用户的集合，在组上附加策略 角色（Role） → 临时身份，EC2/Lambda/跨账号 assume role 策略（Policy） → 权限的 JSON 定义文档 策略类型：\n类型 附加到 说明 Identity-based Policy User/Group/Role 主体有哪些权限 Resource-based Policy S3 Bucket / KMS Key 等 谁可以访问这个资源 SCP（Service Control Policy） AWS Organizations OU 整个账号/OU 的最大权限边界 Instance Profile（实例配置文件）：\n问题：EC2 上的应用需要访问 S3，但不能把 AK/SK 写死在代码里 解决： 1. 创建 IAM Role，附加 S3 访问权限策略 2. 创建 Instance Profile，关联该 Role 3. 启动 EC2 时绑定 Instance Profile 4. 应用通过 169.254.169.254（元数据接口）自动获取临时凭证 Java SDK 会自动从 Instance Metadata 读取临时凭证，无需任何配置 6.2 AWS Secrets Manager vs Parameter Store 特性 Secrets Manager Parameter Store 定位 专为敏感凭据设计 配置参数+敏感信息 自动轮换 ✅（集成 Lambda 定时换密码） ❌（不支持自动轮换） 费用 按密钥收费（每个/月） 标准参数免费，高级参数收费 典型用途 DB密码、API Key、OAuth Token 应用配置、Feature Flag、少量密钥 版本管理 ✅ ✅ 跨 Region 复制 ✅ ❌ 记忆口诀： 需要自动换密码 → Secrets Manager；普通配置/偶尔用的密钥 → Parameter Store（省钱）\n6.3 安全服务矩阵 服务 功能 类比 GuardDuty 威胁检测（异常行为、恶意 IP） 入侵检测系统 IDS Inspector 漏洞扫描（EC2/容器/Lambda） 自动化漏洞扫描器 Security Hub 聚合多账号安全发现 SIEM 聚合面板 Macie S3 数据分类，发现 PII 数据 数据安全分类 DLP Shield DDoS 防护（Standard 免费，Advanced 收费） Anti-DDoS WAF Web 应用防火墙（SQL注入、XSS等） WAF Firewall Manager 多账号统一安全规则管理 集中安全策略 Network Firewall VPC 级深度包检测防火墙 进阶 NACL + IPS KMS 密钥管理，加密/解密服务 HSM 密钥管理 CloudHSM 专用硬件安全模块 物理 HSM 6.4 AWS Organizations \u0026amp; SCP Management Account（根账号） └── Root ├── OU（研发部门） │ ├── Member Account A（Dev环境） │ └── Member Account B（Test环境） └── OU（生产部门） └── Member Account C（Prod环境） SCP（Service Control Policy）作用于 OU 或账号： - 不授予权限，只限制权限上限 - 即使 IAM Policy 允许，SCP 拒绝则无法执行 - 用途：禁止生产账号使用非授权 Region，禁止删除 CloudTrail 七、分析 Analytics 7.1 分析服务全景 数据采集层： Kinesis Data Streams → 实时数据流入（自定义消费） Kinesis Firehose → 数据直接投递到 S3/Redshift/ES（无需代码） AWS Glue → ETL 服务，数据转换清洗 存储层： S3 → 数据湖（Data Lake） Redshift → 数据仓库（Data Warehouse） 查询分析层： Athena → 直接 SQL 查 S3（无服务器，按扫描量计费） Redshift → 复杂 OLAP 分析 EMR → 托管 Hadoop/Spark 集群（大规模数据处理） 可视化层： QuickSight → BI 看板，对接 Athena/Redshift/S3 7.2 Kinesis 三兄弟 服务 定位 类比 Kinesis Data Streams 实时数据流，消费者自定义代码处理 Kafka（自己写消费者） Kinesis Data Firehose 流式数据直接投递到目的地，全托管 Kafka + Fluentd（自动写入） Kinesis Data Analytics 在流数据上执行 SQL/Flink 查询 Flink（实时分析） 7.3 Athena 使用场景 典型场景： S3 存了大量 JSON/CSV/Parquet 日志 → 不想搭 Spark 集群 → 直接在 Athena 写 SQL 查询 → 按扫描数据量计费（Parquet 列式格式可大幅降低扫描量） 成本优化： 原始 JSON → 转换为 Parquet（Glue ETL） → 同样查询成本可降低 ~87% 八、管理与治理 Management 8.1 CloudWatch 核心 CloudWatch 组件： Metrics → 监控数字指标（CPU、网络、自定义指标） Logs → 日志收集存储（Log Group \u0026gt; Log Stream） Alarms → 基于指标触发告警（通知 SNS / 触发 ASG） Dashboards → 可视化面板 Events → 已被 EventBridge 替代 Log Group 层级： Log Group（/aws/lambda/my-function） └── Log Stream（2024/01/01/[$LATEST]abc123） └── Log Events（具体一条日志） 8.2 CloudTrail vs CloudWatch vs Config 服务 回答的问题 数据类型 CloudWatch 系统现在表现如何？（CPU多少？） 指标、日志 CloudTrail 谁在什么时间做了什么操作？ API 调用审计日志 AWS Config 资源配置变更历史是什么？是否合规？ 配置快照、合规规则 记忆： CloudTrail = 人的操作记录；Config = 资源的状态记录；CloudWatch = 系统运行监控\n8.3 AWS SSM（Systems Manager） 核心功能： Session Manager → 无需 SSH/RDP，浏览器直接连 EC2（无需开22端口） Run Command → 批量远程执行命令（不用逐台 SSH） Patch Manager → 自动化补丁管理 Parameter Store → 配置参数存储 Automation → 定义运维操作 Runbook，自动化执行 九、架构设计模式 Patterns 9.1 高可用架构设计原则 核心原则：消除单点故障（SPOF） 多 AZ 部署： EC2 → 跨 AZ 的 ASG + ALB RDS → Multi-AZ（同步复制，自动故障转移） ElastiCache → Multi-AZ with auto-failover 多 Region 部署（更高级别）： Route 53 Failover/Latency 路由 S3 CRR（数据复制） Aurora Global Database（\u0026lt; 1秒 RPO） 9.2 解耦架构模式 紧耦合（反模式）： A服务 → HTTP同步调用 → B服务 问题：B挂了A也挂，B慢了A也慢 松耦合（推荐）： A服务 → SQS Queue → B服务（异步处理） 优点：B挂了消息积压，B恢复后继续消费；峰值流量被队列缓冲 扇出解耦： 一条消息 → SNS Topic → N个 SQS → N个消费者 9.3 常见架构场景解题思路 场景1：降低数据库压力\n方案： 读多写少 → ElastiCache（Redis）缓存热点数据 读扩展 → Aurora 只读副本（最多15个） 会话状态 → ElastiCache 存 Session（不放在 EC2 本地） 场景2：处理突发流量（variable workloads）\n方案： 计算层 → EC2 ASG（自动扩缩容）+ Target Tracking 队列层 → SQS 削峰填谷（前端快速响应，后端慢慢处理） Lambda → 天然支持突发（并发自动扩展） 场景3：本地数据迁移上云\n方案选择： 数据量 \u0026lt; TB 级，网络够用 → AWS DataSync（在线迁移） 数据量 TB~PB 级，网络慢 → AWS Snowball Edge（物理设备） 持续混合云存储 → Storage Gateway 数据库迁移 → AWS DMS（Database Migration Service） 场景4：全球用户访问加速\n静态内容（图片/CSS/JS）→ CloudFront CDN 动态 API 加速 → Global Accelerator 多 Region 部署 → Route 53 Latency-based 路由 场景5：多账号安全治理\n账号管理 → AWS Organizations + OU 结构 权限边界 → SCP（防止账号内 IAM 越权） 安全统一 → Firewall Manager（统一 WAF/SG 规则） 审计合规 → CloudTrail + AWS Config 威胁检测 → GuardDuty（所有账号都开） 十、高频考点速查 10.1 存储类型判断 关键词 选型 \u0026ldquo;共享文件系统\u0026rdquo;、\u0026ldquo;多 EC2 同时挂载\u0026rdquo; EFS \u0026ldquo;数据库磁盘\u0026rdquo;、\u0026ldquo;单实例\u0026rdquo; EBS \u0026ldquo;临时高速缓存\u0026rdquo;、\u0026ldquo;实例停止数据丢失\u0026rdquo; Instance Store \u0026ldquo;对象存储\u0026rdquo;、\u0026ldquo;静态资源\u0026rdquo; S3 \u0026ldquo;Windows 文件共享\u0026rdquo;、\u0026ldquo;SMB\u0026rdquo; FSx for Windows \u0026ldquo;HPC\u0026rdquo;、\u0026ldquo;高性能计算\u0026rdquo; FSx for Lustre 10.2 数据库类型判断 关键词 选型 \u0026ldquo;无 Schema\u0026rdquo;、\u0026ldquo;高并发低延迟\u0026rdquo; DynamoDB \u0026ldquo;DynamoDB 微秒级读\u0026rdquo; DAX \u0026ldquo;高可用 MySQL/PostgreSQL\u0026rdquo;、\u0026ldquo;自动扩展\u0026rdquo; Aurora \u0026ldquo;数据仓库\u0026rdquo;、\u0026ldquo;OLAP\u0026rdquo;、\u0026ldquo;PB级分析\u0026rdquo; Redshift \u0026ldquo;SQL 查询 S3\u0026rdquo; Athena \u0026ldquo;托管 Spark/Hadoop\u0026rdquo; EMR 10.3 消息队列选型 需求 选型 广播给多个消费者 SNS + SQS Fan-out 任务队列、削峰填谷 SQS Standard 严格顺序、不重复 SQS FIFO 实时数据流 Kinesis Data Streams AWS 服务事件触发 EventBridge 10.4 网络连接选型 需求 选型 本地 ↔ AWS 专线，低延迟 Direct Connect 本地 ↔ AWS VPN（快速搭建） Site-to-Site VPN VPC 内访问 S3/DynamoDB（私网） VPC Gateway Endpoint 多 VPC 内网互联 VPC Peering / Transit Gateway 全球用户 HTTP 加速（缓存） CloudFront 全球用户任意协议加速（路由） Global Accelerator 10.5 安全服务选型 需求 选型 检测异常 API 调用、恶意 IP GuardDuty 扫描 EC2 漏洞 Inspector S3 数据中是否有 PII 数据 Macie DDoS 防护 Shield SQL注入/XSS 防护 WAF API 调用审计 CloudTrail 资源配置合规检查 AWS Config 密钥自动轮换 Secrets Manager 应用配置参数存储 Parameter Store ","permalink":"https://lv-blog.pages.dev/posts/programming/backend/aws/","summary":"\u003cblockquote\u003e\n\u003cp\u003e面向系统架构师认证，结合 Java 后端视角整理。核心思路：\u003cstrong\u003e每个服务解决什么痛点、何时选它、和类似服务怎么区分\u003c/strong\u003e。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003ch2 id=\"一存储-storage\"\u003e一、存储 Storage\u003c/h2\u003e\n\u003ch3 id=\"11-三种存储类型对比\"\u003e1.1 三种存储类型对比\u003c/h3\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e类型\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e代表服务\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e访问单元\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e典型场景\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e块存储\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eEBS, EC2 Instance Store\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e数据块（Block）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e数据库、OS 磁盘\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e文件存储\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eEFS, FSx\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e文件/目录树\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e多实例共享、NFS/SMB挂载\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e对象存储\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eS3\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e对象（Object + Key）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e静态资源、备份、数据湖\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003e块存储  → 像本地硬盘，OS看到的是裸设备，自己格式化挂载\n文件存储 → 像 NAS，多台机器可以同时 mount 同一个目录\n对象存储 → 像 HTTP PUT/GET 的 Key-Value，无目录概念，靠前缀模拟\n\u003c/code\u003e\u003c/pre\u003e\u003chr\u003e\n\u003ch3 id=\"12-amazon-s3\"\u003e1.2 Amazon S3\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003e核心功能速查：\u003c/strong\u003e\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e功能\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e说明\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003e常见考点\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eVersioning\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e同一 Key 保留多个历史版本\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e开启后才能用 CRR / MFA Delete\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eCRR（跨区域复制）\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e异步复制到另一 Region\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e灾备 DR、降低延迟\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eTransfer Acceleration\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e通过 CloudFront 边缘节点加速上传\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e上传到遥远 Region 时使用\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eLifecycle Policy\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e对象按年龄自动迁移存储类\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e节省成本核心手段\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eS3 File Gateway\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e本地 SMB/NFS 映射到 S3\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e混合云文件迁移\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003eIntelligent-Tiering\u003c/strong\u003e\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e自动在 Standard ↔ IA 间切换\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e访问模式不可预测时使用\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cp\u003e\u003cstrong\u003eS3 存储类选择决策树：\u003c/strong\u003e\u003c/p\u003e","title":"AWS 学习"},{"content":"1. 服务拆分与远程调用 1.1 为什么要拆分服务？ 单体应用随业务增长面临：部署慢、扩展性差、技术栈固化等问题。微服务将其拆分为独立部署、独立扩缩容的小服务，每个服务只负责一个业务域。\n拆分原则：\n单一职责：每个服务只做一件事 高内聚低耦合：服务内部紧密，服务之间松散 数据独立：每个服务拥有独立数据库 1.2 用户登录流程（微服务视角） Client → Gateway（鉴权） → 业务微服务A → 微服务B（OpenFeign） ↓ Nacos（服务发现） 1.3 RestTemplate — 微服务间原始调用 RestTemplate 是 Spring 提供的 HTTP 客户端，可用于微服务间调用，但代码繁琐、不支持负载均衡，是 OpenFeign 出现前的过渡方案。\n// 注册为 Bean，并开启负载均衡 @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); } // 调用方式（服务名替代 IP:Port） String url = \u0026#34;http://user-service/api/users/\u0026#34; + userId; User user = restTemplate.getForObject(url, User.class); ⚠️ RestTemplate 已逐渐被 OpenFeign 取代，生产中优先选择 OpenFeign。\n2. 服务治理 — Nacos 注册中心 2.1 解决的问题 微服务实例 IP / 端口动态变化，调用方无法硬编码地址。注册中心提供：\n服务注册：服务启动时向注册中心上报自己的地址 服务发现：调用方从注册中心查询目标服务地址，并实现负载均衡 健康检查：注册中心自动剔除不健康的实例 2.2 Nacos 部署（Docker） docker run -d \\ --name nacos \\ -e MODE=standalone \\ -p 8848:8848 \\ nacos/nacos-server:v2.1.0 访问控制台：http://localhost:8848/nacos，默认账号密码均为 nacos。\n2.3 Nacos 命名空间 Nacos 通过命名空间（Namespace） 实现环境隔离，不同命名空间的服务互不可见。\n命名空间 用途 public（默认） 所有环境共用 dev 开发环境 test 测试环境 prod 生产环境 2.4 服务注册 引入依赖：\n\u0026lt;!--Spring Cloud Alibaba 依赖管理--\u0026gt; \u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-alibaba-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2021.0.5.0\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; \u0026lt;!--Nacos 服务发现依赖--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-alibaba-nacos-discovery\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 配置 Nacos 地址（application.yml）：\nspring: application: name: user-service # 服务名，注册到 Nacos 的标识 cloud: nacos: server-addr: localhost:8848 discovery: namespace: dev # 命名空间 ID（非名称） group: DEFAULT_GROUP 2.5 服务发现与负载均衡 Spring Cloud LoadBalancer（Spring 官方）或 Ribbon（Netflix，已停维）会拦截带 @LoadBalanced 注解的 RestTemplate / Feign 请求，从注册中心拉取实例列表并选择一个。\n默认策略为轮询（RoundRobin），可自定义为随机、权重等。\n3. OpenFeign 声明式 HTTP 客户端 3.1 解决的问题 RestTemplate 需要手动拼接 URL、处理参数，代码重复且难以维护。OpenFeign 允许开发者像调用本地方法一样调用远程服务，接口即契约。\n3.2 基础使用 引入依赖：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-openfeign\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 启动类开启 Feign：\n@SpringBootApplication @EnableFeignClients public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } } 定义 FeignClient 接口：\n@FeignClient(value = \u0026#34;user-service\u0026#34;) // 对应 spring.application.name public interface UserClient { @GetMapping(\u0026#34;/api/users/{id}\u0026#34;) User getUserById(@PathVariable(\u0026#34;id\u0026#34;) Long id); @PostMapping(\u0026#34;/api/users\u0026#34;) User createUser(@RequestBody UserDTO dto); } 调用方注入即用：\n@Service @RequiredArgsConstructor public class OrderService { private final UserClient userClient; public OrderVO getOrder(Long orderId) { Order order = orderMapper.selectById(orderId); User user = userClient.getUserById(order.getUserId()); // 像调用本地方法 return buildVO(order, user); } } 3.3 引入 OkHttp 连接池 OpenFeign 默认使用 URLConnection，不带连接池，高并发场景性能差。推荐替换为 OkHttp。\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;io.github.openfeign\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;feign-okhttp\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; feign: okhttp: enabled: true httpclient: ok-http: connection-pool-timeout: PT0.0005S max-connections: 200 max-connections-per-route: 50 3.4 最佳实践 — 抽取 API 模块 问题： 服务提供方和消费方各自维护一份相同的 DTO / FeignClient 接口，导致重复代码。\n方案： 新建 xxx-api 公共模块，将 FeignClient 接口、DTO、异常类统一放在此处。\n项目结构： ├── user-service # 用户服务（提供者） ├── order-service # 订单服务（消费者） └── hm-api # 公共 API 模块 └── client └── UserClient.java └── dto └── UserDTO.java 消费方引入 API 模块依赖：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.hmall\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;hm-api\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;${project.version}\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 解决扫描包不一致问题：\n当 FeignClient 不在 @SpringBootApplication 的扫描包内时，需要显式指定：\n// 方式一：指定扫描包路径 @EnableFeignClients(basePackages = \u0026#34;com.hmall.api.client\u0026#34;) // 方式二：指定 FeignClient 类（更精确） @EnableFeignClients(clients = {UserClient.class, ItemClient.class}) 3.5 日志配置 Feign 日志级别分为 4 级：NONE（默认）、BASIC、HEADERS、FULL。\n// 全局配置 Bean @Bean public Logger.Level feignLogLevel() { return Logger.Level.FULL; } # 针对特定 FeignClient 开启日志（需配合 logging.level） logging: level: com.hmall.api.client.UserClient: DEBUG 4. Spring Cloud Gateway 网关 4.1 解决的问题 客户端直接调用各微服务面临：\n各服务各自鉴权，逻辑重复 暴露内部服务地址，存在安全隐患 跨域、限流、日志等横切关注点分散 网关统一处理上述问题，是微服务体系的统一入口。\n4.2 引入依赖 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-gateway\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--网关也需要注册到 Nacos 以完成服务发现路由--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-alibaba-nacos-discovery\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; ⚠️ 网关基于 WebFlux（响应式），不能引入 spring-boot-starter-web，两者冲突！\n4.3 路由配置 spring: cloud: gateway: routes: - id: user-route # 路由 ID，唯一 uri: lb://user-service # lb:// 表示负载均衡到 Nacos 中的服务 predicates: - Path=/api/users/** # 路径断言 filters: - StripPrefix=1 # 去掉路径前缀 - id: order-route uri: lb://order-service predicates: - Path=/api/orders/** - Method=GET,POST # 方法断言 - Header=X-Request-Id, \\d+ # 请求头断言（正则） filters: - AddRequestHeader=X-Source, gateway # 添加请求头 常用路由断言工厂（Predicate）：\n断言工厂 说明 Path 路径匹配 Method HTTP 方法匹配 Header 请求头匹配（支持正则） Query 请求参数匹配 After/Before/Between 时间断言 RemoteAddr IP 地址断言 4.4 网关请求处理流程 Client Request ↓ HttpWebHandlerAdapter ↓ DispatcherHandler ↓ RoutePredicateHandlerMapping ← 匹配路由 ↓ FilteringWebHandler ↓ [Global Filters] → [GatewayFilter Chain] → NettyRoutingFilter（实际转发） ↓ Proxied Service 4.5 网关登录校验 — 自定义 GlobalFilter @Component @RequiredArgsConstructor @Order(-1) // 优先级，数字越小越先执行 public class AuthGlobalFilter implements GlobalFilter { private final JwtTool jwtTool; // 白名单路径（不需要鉴权） private static final List\u0026lt;String\u0026gt; WHITE_LIST = List.of( \u0026#34;/api/users/login\u0026#34;, \u0026#34;/api/users/register\u0026#34; ); @Override public Mono\u0026lt;Void\u0026gt; filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String path = request.getPath().value(); // 1. 白名单放行 if (isWhitePath(path)) { return chain.filter(exchange); } // 2. 获取 Token String token = getTokenFromRequest(request); if (token == null) { return unauthorized(exchange); } // 3. 解析 Token Long userId; try { userId = jwtTool.parseToken(token); } catch (Exception e) { return unauthorized(exchange); } // 4. 将用户 ID 传递给下游微服务 ServerWebExchange mutatedExchange = exchange.mutate() .request(builder -\u0026gt; builder.header(\u0026#34;X-User-Id\u0026#34;, userId.toString())) .build(); return chain.filter(mutatedExchange); } private boolean isWhitePath(String path) { return WHITE_LIST.stream().anyMatch(path::startsWith); } private String getTokenFromRequest(ServerHttpRequest request) { List\u0026lt;String\u0026gt; headers = request.getHeaders().get(\u0026#34;Authorization\u0026#34;); if (headers == null || headers.isEmpty()) return null; String auth = headers.get(0); return auth.startsWith(\u0026#34;Bearer \u0026#34;) ? auth.substring(7) : null; } private Mono\u0026lt;Void\u0026gt; unauthorized(ServerWebExchange exchange) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } } 4.6 微服务中获取当前用户 — MVC 拦截器 在 common 模块中编写拦截器，从请求头获取网关传递的用户 ID，并存入 UserContext（ThreadLocal）：\npublic class UserInfoInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String userIdStr = request.getHeader(\u0026#34;X-User-Id\u0026#34;); if (StringUtils.hasText(userIdStr)) { UserContext.setUser(Long.parseLong(userIdStr)); } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.removeUser(); // 防止内存泄漏！ } } // ThreadLocal 工具类 public class UserContext { private static final ThreadLocal\u0026lt;Long\u0026gt; TL = new ThreadLocal\u0026lt;\u0026gt;(); public static void setUser(Long userId) { TL.set(userId); } public static Long getUser() { return TL.get(); } public static void removeUser() { TL.remove(); } } 避免网关引入 MVC 自动配置问题：\n将 MvcConfig（注册拦截器的配置类）通过 spring.factories 自动装配，并在网关模块中排除：\n// common 模块：resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports com.hmall.common.config.MvcConfig # 网关模块 application.yml 中排除 spring: autoconfigure: exclude: - com.hmall.common.config.MvcConfig 4.7 OpenFeign 传递用户 — RequestInterceptor 微服务 A 通过 Feign 调用微服务 B 时，需要将当前用户 ID 带过去：\n@Bean public RequestInterceptor userInfoRequestInterceptor() { return template -\u0026gt; { Long userId = UserContext.getUser(); if (userId != null) { template.header(\u0026#34;X-User-Id\u0026#34;, userId.toString()); } }; } 所有 OpenFeign 发出的请求都会先经过 RequestInterceptor，自动携带用户信息。\n5. 配置管理 — Nacos Config 5.1 存在意义 痛点 Nacos Config 解决方案 多服务相同配置重复写 配置共享：多服务读同一份配置 修改配置需重启服务 配置热更新：运行中动态生效 不同环境配置混乱 通过 Namespace + Group 隔离 5.2 引入依赖 \u0026lt;!--Nacos 配置中心--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-alibaba-nacos-config\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!--引导上下文，用于在应用启动前加载 Nacos 配置--\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-bootstrap\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; 5.3 bootstrap.yml vs application.yml 对比项 bootstrap.yml application.yml 加载时机 应用启动前（引导阶段） 应用启动时 用途 连接配置中心、加密密钥等 业务配置 优先级 低（被 application.yml 覆盖） 高 # bootstrap.yml — 拉取 Nacos 共享配置 spring: application: name: user-service profiles: active: dev cloud: nacos: server-addr: localhost:8848 config: file-extension: yaml shared-configs: - data-id: shared-jdbc.yaml # 数据库共享配置 group: DEFAULT_GROUP refresh: true - data-id: shared-redis.yaml # Redis 共享配置 group: DEFAULT_GROUP refresh: true # 服务自己的配置：user-service-dev.yaml（自动识别） 5.4 配置热更新 在需要热更新的配置属性对应的 Bean 上添加注解：\n// 方式一：@RefreshScope（整个 Bean 刷新） @Component @RefreshScope public class CartProperties { @Value(\u0026#34;${cart.max-amount:100}\u0026#34;) private Integer maxAmount; } // 方式二：@ConfigurationProperties（推荐，更安全） @Component @ConfigurationProperties(prefix = \u0026#34;cart\u0026#34;) @Data public class CartProperties { private Integer maxAmount = 100; // Nacos 中配置变更后自动刷新，无需 @RefreshScope } 5.5 动态路由 网关路由规则也可存放在 Nacos 中，实现不重启网关动态更新路由：\n@Component @RequiredArgsConstructor public class DynamicRouteLoader implements ApplicationRunner { private final RouteDefinitionWriter writer; private final NacosConfigManager nacosConfigManager; private static final String DATA_ID = \u0026#34;gateway-routes.json\u0026#34;; private static final String GROUP = \u0026#34;DEFAULT_GROUP\u0026#34;; @Override public void run(ApplicationArguments args) throws Exception { // 首次拉取 loadRoutes(); // 监听变化 nacosConfigManager.getConfigService().addListener(DATA_ID, GROUP, new Listener() { @Override public void receiveConfigInfo(String configInfo) { loadRoutes(); } @Override public Executor getExecutor() { return null; } }); } private void loadRoutes() { // 解析 JSON 并注册路由定义 // ... } } 6. 微服务保护 — Sentinel 6.1 雪崩问题（Cascading Failure） 服务 D 宕机 → 线程在 C 中堆积等待 D → C 也耗尽线程 → B 也... → 整个系统崩溃 四种保护手段：\n手段 场景 工具 请求限流 预防流量激增 Sentinel 流控规则 线程隔离 避免故障服务拖垮调用方 Sentinel 隔离 / Hystrix 舱壁模式 服务熔断 异常比例过高时快速失败 Sentinel 断路器 Fallback 降级时的兜底逻辑 FallbackFactory 6.2 引入 Sentinel \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-alibaba-sentinel\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; spring: cloud: sentinel: transport: dashboard: localhost:8090 # Sentinel 控制台地址 eager: true # 提前初始化，避免首次请求看不到资源 启动 Sentinel 控制台：\njava -Dserver.port=8090 \\ -Dcsp.sentinel.dashboard.server=localhost:8090 \\ -Dproject.name=sentinel-dashboard \\ -jar sentinel-dashboard.jar 6.3 请求限流（流控） 流控规则在 Sentinel 控制台中配置，核心参数：\n参数 说明 QPS（Queries Per Second） 每秒请求数阈值 并发线程数 同时处理的线程数阈值 流控模式 直接 / 关联 / 链路 流控效果 快速失败 / 排队等待（匀速） / 预热 排队等待（漏桶算法）适合削峰填谷场景。\n6.4 线程隔离 Sentinel 采用信号量隔离（非线程池隔离），资源开销小：\n在控制台对某个簇点资源配置「并发线程数」流控，超出阈值的请求直接走 Fallback，不会无限堆积。\n簇点资源名称重复问题： 当不同路径的 OpenFeign 调用映射到相同方法时，Sentinel 的簇点链路会出现重名，需保证 requestUri 唯一或通过注解自定义资源名。\n6.5 Fallback（降级兜底） 让 OpenFeign 调用纳入 Sentinel 管理，并编写降级逻辑：\nfeign: sentinel: enabled: true // 1. 实现 FallbackFactory @Component public class UserClientFallbackFactory implements FallbackFactory\u0026lt;UserClient\u0026gt; { @Override public UserClient create(Throwable cause) { log.error(\u0026#34;UserClient 调用失败\u0026#34;, cause); return new UserClient() { @Override public User getUserById(Long id) { // 返回默认值或空对象，避免 NPE return User.builder().id(id).name(\u0026#34;未知用户\u0026#34;).build(); } }; } } // 2. 在 FeignClient 中引用 Factory @FeignClient(value = \u0026#34;user-service\u0026#34;, fallbackFactory = UserClientFallbackFactory.class) public interface UserClient { @GetMapping(\u0026#34;/api/users/{id}\u0026#34;) User getUserById(@PathVariable(\u0026#34;id\u0026#34;) Long id); } // 3. 将 Factory 注册为 Bean（若在 hm-api 模块中，需确保被扫描） @Bean public UserClientFallbackFactory userClientFallbackFactory() { return new UserClientFallbackFactory(); } 6.6 服务熔断（断路器） Sentinel 断路器基于三种状态机：\n正常 ──────────────────────────────────────────► Closed（关闭，正常请求） ↓ 异常比例/慢调用比例超阈值 Open（打开，快速失败，不再调用下游） ↓ 熔断时长结束 Half-Open（半开，放一个探测请求） ↓ 成功 ↓ 失败 Closed Open 在控制台配置熔断规则：\n慢调用比例： RT 超过阈值的请求占比触发熔断 异常比例： 异常请求占总请求比例触发熔断 异常数： 统计窗口内异常数超阈值触发熔断 6.7 Tomcat 线程数配置 配合 Sentinel 线程隔离，合理控制 Tomcat 线程池大小：\nserver: port: 8082 tomcat: threads: max: 25 # 最大工作线程数 min-spare: 5 # 最小空闲线程数 accept-count: 25 # 等待队列大小 max-connections: 100 7. 分布式事务 — Seata 7.1 问题背景 订单服务（扣库存）→ 库存服务（扣库存）→ 积分服务（加积分） 三个操作跨三个数据库，本地 @Transactional 只能保证单个数据库的原子性，微服务架构中需要分布式事务保证跨服务的一致性。\n分布式事务核心概念：\n全局事务（Global Transaction）： 跨多个服务/数据库的一次完整业务操作 分支事务（Branch Transaction）： 全局事务中每个服务内的本地事务 TC（Transaction Coordinator）： 事务协调者，维护全局和分支事务状态 TM（Transaction Manager）： 事务发起方，定义全局事务的开始和结束 RM（Resource Manager）： 各微服务，管理分支事务 7.2 Seata 部署（Docker） 第一步：准备 TC 存储的数据库表\nCREATE DATABASE IF NOT EXISTS `seata`; USE `seata`; CREATE TABLE IF NOT EXISTS `global_table` ( `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `status` TINYINT NOT NULL, `application_id` VARCHAR(32), `transaction_service_group` VARCHAR(32), `transaction_name` VARCHAR(128), `timeout` INT, `begin_time` BIGINT, `application_data` VARCHAR(2000), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4; -- branch_table、lock_table、distributed_lock 同理（见官方文档） 第二步：启动 Seata Server\ndocker run --name seata \\ -p 8099:8099 \\ -p 7099:7099 \\ -e SEATA_IP=192.168.184.129 \\ -v ./seata:/seata-server/resources \\ --privileged=true \\ --network testNet \\ -d seataio/seata-server:1.5.2 7099：Web 控制台 8099：微服务注册端口 7.3 微服务集成 Seata \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba.cloud\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-cloud-starter-alibaba-seata\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; seata: registry: type: nacos nacos: server-addr: localhost:8848 namespace: \u0026#34;\u0026#34; group: DEFAULT_GROUP application: seata-server # Seata Server 在 Nacos 中的服务名 tx-service-group: hmall # 事务组名称 service: vgroup-mapping: hmall: default # 事务组 → 集群映射 data-source-proxy-mode: AT # AT 模式（默认） 事务发起方加注解：\n@Service @RequiredArgsConstructor public class OrderService { @GlobalTransactional(name = \u0026#34;create-order\u0026#34;, rollbackFor = Exception.class) public void createOrder(OrderDTO dto) { // 1. 扣减库存（远程调用 → Seata RM 注册分支事务） itemClient.deductStock(dto.getItemId(), dto.getNum()); // 2. 扣减余额（远程调用 → Seata RM 注册分支事务） userClient.deductBalance(dto.getUserId(), dto.getAmount()); // 3. 创建订单（本地操作 → Seata RM 注册分支事务） save(buildOrder(dto)); } } 7.4 XA 模式 vs AT 模式 XA 模式 一阶段：TM 通知各 RM 执行 SQL 但不提交 → 锁定资源 二阶段：所有分支成功 → TM 通知提交；任一失败 → TM 通知全部回滚 seata: data-source-proxy-mode: XA // 开启 XA 支持 @Bean @ConfigurationProperties(prefix = \u0026#34;spring.datasource\u0026#34;) public DataSource dataSource(DruidDataSourceWrapper druidDataSourceWrapper) { return new DataSourceProxyXA(druidDataSourceWrapper); } 特点：\n✅ 强一致性（二阶段结束前数据不可见） ❌ 资源锁定时间长，吞吐量低 AT 模式（推荐） 一阶段：记录 undo log（快照）→ 直接提交本地事务（释放锁） 二阶段成功：删除 undo log 二阶段失败：通过 undo log 反向补偿回滚 每个参与 AT 模式的数据库需要创建 undo_log 表：\nCREATE TABLE IF NOT EXISTS `undo_log` ( `branch_id` BIGINT NOT NULL COMMENT \u0026#39;branch transaction id\u0026#39;, `xid` VARCHAR(128) NOT NULL COMMENT \u0026#39;global transaction id\u0026#39;, `context` VARCHAR(128) NOT NULL COMMENT \u0026#39;serialization\u0026#39;, `rollback_info` LONGBLOB NOT NULL COMMENT \u0026#39;rollback info\u0026#39;, `log_status` INT(11) NOT NULL COMMENT \u0026#39;0:normal,1:defense\u0026#39;, `log_created` DATETIME(6) NOT NULL, `log_modified` DATETIME(6) NOT NULL, UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`) ) ENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4; 特点：\n✅ 吞吐量高（一阶段即释放锁） ⚠️ 最终一致性（存在短暂数据不一致窗口） 两种模式对比 对比项 XA AT 一致性 强一致 最终一致 锁粒度 数据库锁（长时间） 行锁（短时间） 性能 低 高 适用场景 金融核心交易 电商、一般业务 7.5 @GlobalTransactional vs @Transactional 注解 范围 协调者 @Transactional 单个服务的本地事务 本地数据库 @GlobalTransactional 跨服务的全局事务 Seata TC 两者可以共存：@GlobalTransactional 在 TM 侧控制全局，@Transactional 在 RM 侧控制本地。\n8. 消息队列 — RabbitMQ 8.1 同步调用 vs 异步调用 对比项 同步（OpenFeign） 异步（MQ） 耦合度 高（强依赖对方可用性） 低（通过 Broker 解耦） 性能 串行，吞吐低 并行，吞吐高 可靠性 调用方等待响应 消息持久化，可重试 适用场景 核心业务（需要即时结果） 边缘业务（日志、通知、积分） 8.2 RabbitMQ 核心概念 Publisher → Exchange → [Binding] → Queue → Consumer 概念 说明 Publisher 消息发送方 Exchange 交换机，负责路由 Queue 消息队列，消息存储 Consumer 消息消费方 Virtual Host 虚拟主机，数据隔离单元 Binding Key 队列绑定到交换机的规则 8.3 Spring AMQP 快速上手 \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-amqp\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; spring: rabbitmq: host: localhost port: 5672 virtual-host: /hmall username: hmall password: 123456 发送消息：\n@Service @RequiredArgsConstructor public class PayService { private final RabbitTemplate rabbitTemplate; public void notifyPaySuccess(Long orderId) { rabbitTemplate.convertAndSend( \u0026#34;pay.direct\u0026#34;, // exchange \u0026#34;pay.success\u0026#34;, // routing key orderId // message body（使用 JSON 转换器时会自动序列化） ); } } 消费消息：\n@Component @Slf4j public class PayMessageListener { @RabbitListener(queues = \u0026#34;pay.queue\u0026#34;) public void onPaySuccess(Long orderId) { log.info(\u0026#34;收到支付成功消息，订单 ID: {}\u0026#34;, orderId); // 更新订单状态 } } 8.4 三种交换机类型 FanoutExchange（广播） // 声明方式（Consumer 侧） @Bean public FanoutExchange fanoutExchange() { return new FanoutExchange(\u0026#34;hmall.fanout\u0026#34;); } @Bean public Queue fanoutQueue1() { return QueueBuilder.durable(\u0026#34;fanout.queue1\u0026#34;).build(); } @Bean public Binding binding1(Queue fanoutQueue1, FanoutExchange fanoutExchange) { return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange); } DirectExchange（定向路由） // 基于注解声明（更简洁，推荐） @RabbitListener(bindings = @QueueBinding( value = @Queue(name = \u0026#34;direct.queue1\u0026#34;, durable = \u0026#34;true\u0026#34;), exchange = @Exchange(name = \u0026#34;hmall.direct\u0026#34;, type = ExchangeTypes.DIRECT), key = {\u0026#34;red\u0026#34;, \u0026#34;blue\u0026#34;} )) public void listenDirectQueue1(String message) { log.info(\u0026#34;direct.queue1 收到消息: {}\u0026#34;, message); } TopicExchange（主题路由，通配符） 通配符 含义 * 匹配一个单词 # 匹配零个或多个单词 @RabbitListener(bindings = @QueueBinding( value = @Queue(name = \u0026#34;topic.queue1\u0026#34;, durable = \u0026#34;true\u0026#34;), exchange = @Exchange(name = \u0026#34;hmall.topic\u0026#34;, type = ExchangeTypes.TOPIC), key = \u0026#34;china.#\u0026#34; // 匹配所有以 china. 开头的 routing key )) public void listenTopicQueue1(String message) { log.info(\u0026#34;topic.queue1 收到消息: {}\u0026#34;, message); } 8.5 消息转换器（JSON） 默认使用 JDK 序列化：安全性差、体积大、可读性差。推荐替换为 Jackson JSON：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.fasterxml.jackson.dataformat\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;jackson-dataformat-xml\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; // Publisher 和 Consumer 都需要配置 @Bean public MessageConverter jacksonMessageConverter() { Jackson2JsonMessageConverter converter = new Jackson2JsonMessageConverter(); converter.setCreateMessageIds(true); // 自动生成消息 ID，用于幂等判断 return converter; } 8.6 消费者消息推送限制（预取计数） 默认 RabbitMQ 将所有消息均匀分配给消费者，不考虑消费者的处理能力。\nspring: rabbitmq: listener: simple: prefetch: 1 # 同一时刻最多投递 1 条消息给消费者，处理完才能接收下一条 效果： 处理快的消费者会自动处理更多消息（能者多劳），避免消息堆积。\n8.7 消息可靠性保障 发送者确认机制 spring: rabbitmq: publisher-confirm-type: correlated # 异步回调确认（推荐） publisher-returns: true # 开启路由失败回调 template: mandatory: true @PostConstruct // Bean 初始化后执行 public void init() { // 路由失败时回调（消息到达 Exchange 但未路由到 Queue） rabbitTemplate.setReturnsCallback(returnedMessage -\u0026gt; { log.error(\u0026#34;消息路由失败: exchange={}, routingKey={}, message={}\u0026#34;, returnedMessage.getExchange(), returnedMessage.getRoutingKey(), returnedMessage.getMessage()); // 重新发送或记录日志 }); } public void sendWithConfirm(String exchange, String key, Object msg) { CorrelationData cd = new CorrelationData(UUID.randomUUID().toString()); cd.getFuture().addCallback( confirm -\u0026gt; { if (confirm.isAck()) { log.info(\u0026#34;消息成功到达 Exchange，ID: {}\u0026#34;, cd.getId()); } else { log.error(\u0026#34;消息未到达 Exchange，原因: {}，ID: {}\u0026#34;, confirm.getReason(), cd.getId()); // 重发逻辑 } }, throwable -\u0026gt; log.error(\u0026#34;消息发送异常\u0026#34;, throwable) ); rabbitTemplate.convertAndSend(exchange, key, msg, cd); } 什么时候可以确认消息发送到了队列？\nConfirmCallback.onSuccess 收到 ACK → 消息已到达 Exchange ReturnsCallback 没有被触发 → 消息路由到 Queue 成功 MQ 数据持久化 // 交换机持久化（默认 durable=true） @Bean public DirectExchange payExchange() { return ExchangeBuilder.directExchange(\u0026#34;pay.direct\u0026#34;).durable(true).build(); } // 队列持久化 @Bean public Queue payQueue() { return QueueBuilder.durable(\u0026#34;pay.queue\u0026#34;).build(); } // 消息持久化（Spring AMQP 默认 persistent） rabbitTemplate.convertAndSend(exchange, key, msg, message -\u0026gt; { message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT); return message; }); 惰性队列（Lazy Queue） 将消息直接写入磁盘而非内存，适合消息堆积场景：\n// Bean 方式 @Bean public Queue lazyQueue() { return QueueBuilder.durable(\u0026#34;lazy.queue\u0026#34;) .lazy() // 设置为惰性队列 .build(); } // 注解方式 @RabbitListener(bindings = @QueueBinding( value = @Queue( name = \u0026#34;lazy.queue\u0026#34;, durable = \u0026#34;true\u0026#34;, arguments = @Argument(name = \u0026#34;x-queue-mode\u0026#34;, value = \u0026#34;lazy\u0026#34;) ), ... )) 8.8 消费者可靠性 消费者确认机制 spring: rabbitmq: listener: simple: acknowledge-mode: auto # 推荐：环绕增强，自动 ack/nack # none：立即 ack，不安全 # manual：手动 ack，灵活但代码侵入性强 auto 模式下：\n方法正常执行 → 自动 ack 抛出异常 → 自动 nack，消息重新入队 抛出 AmqpRejectAndDontRequeueException → 直接丢弃 失败重试策略 spring: rabbitmq: listener: simple: retry: enabled: true initial-interval: 1000ms # 初始重试间隔 multiplier: 1.0 # 间隔倍数 max-attempts: 3 # 最大重试次数 stateless: true # 无状态重试 重试耗尽后的处理策略：\n@Bean public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate) { // 推荐：重试耗尽后将失败消息投递到指定\u0026#34;死信交换机\u0026#34; return new RepublishMessageRecoverer(rabbitTemplate, \u0026#34;error.direct\u0026#34;, \u0026#34;error\u0026#34;); } 策略 说明 RejectAndDontRequeueRecover 直接丢弃（默认） ImmediateRequeueMessageRecover 重新入队（可能导致死循环） RepublishMessageRecover 发送到错误队列（推荐） 8.9 业务幂等性 由于消息可能被重复消费，业务逻辑需要保证幂等性：\n@RabbitListener(queues = \u0026#34;pay.queue\u0026#34;) public void onPaySuccess(Message message) { String msgId = message.getMessageProperties().getMessageId(); // 方式一：利用消息 ID + Redis 去重 Boolean isFirst = redisTemplate.opsForValue() .setIfAbsent(\u0026#34;pay:msg:\u0026#34; + msgId, \u0026#34;1\u0026#34;, 30, TimeUnit.MINUTES); if (Boolean.FALSE.equals(isFirst)) { log.warn(\u0026#34;重复消息，忽略: {}\u0026#34;, msgId); return; } Long orderId = (Long) rabbitTemplate.getMessageConverter().fromMessage(message); // 方式二：业务状态判断 Order order = orderService.getById(orderId); if (order.getStatus() != OrderStatus.UNPAID) { log.warn(\u0026#34;订单状态已更新，忽略重复消息: {}\u0026#34;, orderId); return; } orderService.updateStatus(orderId, OrderStatus.PAID); } 8.10 延迟消息 方式一：死信交换机（TTL + DLX） @Bean public Queue ttlQueue() { return QueueBuilder.durable(\u0026#34;ttl.queue\u0026#34;) .ttl(30000) // 消息存活 30 秒 .deadLetterExchange(\u0026#34;dlx.direct\u0026#34;) // 过期后转发到死信交换机 .deadLetterRoutingKey(\u0026#34;dlx.order\u0026#34;) .build(); } ⚠️ 缺点：队列头部消息未过期会阻塞后续消息，即使后续消息已过期。\n方式二：RabbitMQ 延迟消息插件（推荐） 安装 rabbitmq_delayed_message_exchange 插件后：\n@Bean public DirectExchange delayedExchange() { return ExchangeBuilder.directExchange(\u0026#34;delayed.exchange\u0026#34;) .delayed() // 声明为延迟交换机 .durable(true) .build(); } // 发送时指定延迟时间 rabbitTemplate.convertAndSend(\u0026#34;delayed.exchange\u0026#34;, \u0026#34;delay.key\u0026#34;, orderId, message -\u0026gt; { message.getMessageProperties().setDelayLong(30 * 60 * 1000L); // 延迟 30 分钟 return message; }); 8.11 面试高频题 Q：如何保证支付服务与交易服务之间的订单状态一致性？\n支付成功后，支付服务通过 MQ 发送消息通知交易服务同步订单状态 可靠性保障：生产者确认（ConfirmCallback + ReturnCallback）+ 消费者确认（auto 模式）+ 消费者失败重试 + MQ 持久化（交换机/队列/消息） 幂等性保障：交易服务更新订单前先判断当前状态，防止重复消费导致异常 Q：如果交易服务消息处理失败，有没有兜底方案？\nMQ 侧： 失败重试 + RepublishMessageRecoverer 将失败消息投递到错误队列，人工或定时任务处理 业务侧： 定时任务扫描长时间未支付/状态未同步的订单，主动向支付服务查询支付结果（补偿机制） 监控侧： 对错误队列和消费延迟配置告警 总结 问题 解决方案 微服务之间 HTTP 调用繁琐 OpenFeign 服务实例动态变化，无法硬编码地址 Nacos 注册中心 各微服务各自鉴权，重复逻辑 Spring Cloud Gateway 配置分散，修改需重启 Nacos Config 服务雪崩 / 流量激增 Sentinel 跨服务数据一致性 Seata 服务间强耦合、同步调用性能差 RabbitMQ ","permalink":"https://lv-blog.pages.dev/posts/programming/backend/spring-cloud/","summary":"\u003ch2 id=\"1-服务拆分与远程调用\"\u003e1. 服务拆分与远程调用\u003c/h2\u003e\n\u003ch3 id=\"11-为什么要拆分服务\"\u003e1.1 为什么要拆分服务？\u003c/h3\u003e\n\u003cp\u003e单体应用随业务增长面临：部署慢、扩展性差、技术栈固化等问题。微服务将其拆分为\u003cstrong\u003e独立部署、独立扩缩容\u003c/strong\u003e的小服务，每个服务只负责一个业务域。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e拆分原则：\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e单一职责：每个服务只做一件事\u003c/li\u003e\n\u003cli\u003e高内聚低耦合：服务内部紧密，服务之间松散\u003c/li\u003e\n\u003cli\u003e数据独立：每个服务拥有独立数据库\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"12-用户登录流程微服务视角\"\u003e1.2 用户登录流程（微服务视角）\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eClient → Gateway（鉴权） → 业务微服务A → 微服务B（OpenFeign）\n                ↓\n           Nacos（服务发现）\n\u003c/code\u003e\u003c/pre\u003e\u003ch3 id=\"13-resttemplate--微服务间原始调用\"\u003e1.3 RestTemplate — 微服务间原始调用\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003eRestTemplate\u003c/code\u003e 是 Spring 提供的 HTTP 客户端，可用于微服务间调用，但\u003cstrong\u003e代码繁琐、不支持负载均衡\u003c/strong\u003e，是 OpenFeign 出现前的过渡方案。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 注册为 Bean，并开启负载均衡\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e@Bean\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e@LoadBalanced\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e RestTemplate \u003cspan style=\"color:#a6e22e\"\u003erestTemplate\u003c/span\u003e() {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e RestTemplate();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 调用方式（服务名替代 IP:Port）\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eString url \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e \u003cspan style=\"color:#e6db74\"\u003e\u0026#34;http://user-service/api/users/\u0026#34;\u003c/span\u003e \u003cspan style=\"color:#f92672\"\u003e+\u003c/span\u003e userId;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003eUser user \u003cspan style=\"color:#f92672\"\u003e=\u003c/span\u003e restTemplate.\u003cspan style=\"color:#a6e22e\"\u003egetForObject\u003c/span\u003e(url, User.\u003cspan style=\"color:#a6e22e\"\u003eclass\u003c/span\u003e);\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cblockquote\u003e\n\u003cp\u003e⚠️ RestTemplate 已逐渐被 OpenFeign 取代，生产中优先选择 OpenFeign。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch2 id=\"2-服务治理--nacos-注册中心\"\u003e2. 服务治理 — Nacos 注册中心\u003c/h2\u003e\n\u003ch3 id=\"21-解决的问题\"\u003e2.1 解决的问题\u003c/h3\u003e\n\u003cp\u003e微服务实例 IP / 端口动态变化，调用方无法硬编码地址。注册中心提供：\u003c/p\u003e","title":"Spring Cloud 学习"},{"content":"一、Spring IoC 容器 1.1 容器体系结构 BeanFactory（顶层接口） └── ApplicationContext（常用接口，扩展了BF） ├── ClassPathXmlApplicationContext（XML配置） ├── FileSystemXmlApplicationContext（文件路径XML） └── AnnotationConfigApplicationContext（注解配置） BeanFactory vs ApplicationContext 核心区别：\n特性 BeanFactory ApplicationContext Bean 初始化时机 懒加载（第一次 getBean 时） 饿加载（容器启动时） 功能 基础 IoC IoC + 事件发布 + 国际化 + AOP等 使用场景 资源极度受限的嵌入式 99% 的业务场景 ApplicationContext 为什么没有 close()？\nApplicationContext 接口本身不定义 close()，是为了保持接口的通用性（不是所有容器都能/需要被关闭，如 Web 容器）。但其实现类 AbstractApplicationContext 实现了 Closeable，可以强转后调用，或用 ConfigurableApplicationContext 接口接收。\n1.2 延迟加载（Lazy Loading） // 注解方式：@Lazy 让 Bean 在第一次被使用时才初始化 @Bean @Lazy public HeavyService heavyService() { return new HeavyService(); } // XML方式： // \u0026lt;bean id=\u0026#34;heavyService\u0026#34; class=\u0026#34;...\u0026#34; lazy-init=\u0026#34;true\u0026#34;/\u0026gt; 适用场景：初始化代价高、启动时不一定用到的 Bean（如某些连接池、第三方SDK客户端）。\n1.3 依赖注入方式 构造器注入（推荐） @Component public class UserService { private final UserMapper userMapper; // final 保证不可变 // Spring 4.3+ 单构造器可省略 @Autowired public UserService(UserMapper userMapper) { this.userMapper = userMapper; } } \u0026lt;!-- XML 构造器注入 --\u0026gt; \u0026lt;bean id=\u0026#34;userService\u0026#34; class=\u0026#34;com.example.UserService\u0026#34;\u0026gt; \u0026lt;!-- 引用类型 --\u0026gt; \u0026lt;constructor-arg name=\u0026#34;userMapper\u0026#34; ref=\u0026#34;userMapper\u0026#34;/\u0026gt; \u0026lt;!-- 简单类型 --\u0026gt; \u0026lt;constructor-arg name=\u0026#34;maxRetry\u0026#34; value=\u0026#34;3\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; Setter 注入 @Component public class OrderService { private UserService userService; // XML方式需要有 setter public void setUserService(UserService userService) { this.userService = userService; } } \u0026lt;!-- XML setter 注入 --\u0026gt; \u0026lt;bean id=\u0026#34;orderService\u0026#34; class=\u0026#34;com.example.OrderService\u0026#34;\u0026gt; \u0026lt;property name=\u0026#34;userService\u0026#34; ref=\u0026#34;userService\u0026#34;/\u0026gt; \u0026lt;property name=\u0026#34;timeout\u0026#34; value=\u0026#34;3000\u0026#34;/\u0026gt; \u0026lt;/bean\u0026gt; 集合注入 \u0026lt;bean id=\u0026#34;dataConfig\u0026#34; class=\u0026#34;com.example.DataConfig\u0026#34;\u0026gt; \u0026lt;!-- List --\u0026gt; \u0026lt;property name=\u0026#34;servers\u0026#34;\u0026gt; \u0026lt;list\u0026gt; \u0026lt;value\u0026gt;192.168.1.1\u0026lt;/value\u0026gt; \u0026lt;value\u0026gt;192.168.1.2\u0026lt;/value\u0026gt; \u0026lt;/list\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;!-- Map --\u0026gt; \u0026lt;property name=\u0026#34;params\u0026#34;\u0026gt; \u0026lt;map\u0026gt; \u0026lt;entry key=\u0026#34;timeout\u0026#34; value=\u0026#34;3000\u0026#34;/\u0026gt; \u0026lt;entry key=\u0026#34;retry\u0026#34; value=\u0026#34;3\u0026#34;/\u0026gt; \u0026lt;/map\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;!-- Properties --\u0026gt; \u0026lt;property name=\u0026#34;jdbcProps\u0026#34;\u0026gt; \u0026lt;props\u0026gt; \u0026lt;prop key=\u0026#34;url\u0026#34;\u0026gt;jdbc:mysql://localhost:3306/db\u0026lt;/prop\u0026gt; \u0026lt;/props\u0026gt; \u0026lt;/property\u0026gt; \u0026lt;/bean\u0026gt; 二、Spring 注解开发 2.1 核心注解速查 注解 作用 等价XML @Component 通用Bean声明 \u0026lt;bean\u0026gt; @Service Service层 \u0026lt;bean\u0026gt; @Repository DAO层 \u0026lt;bean\u0026gt; @Controller MVC控制层 \u0026lt;bean\u0026gt; @Configuration 配置类 \u0026lt;beans\u0026gt; @ComponentScan 开启组件扫描 \u0026lt;context:component-scan\u0026gt; @Bean 方法产生Bean \u0026lt;bean\u0026gt; @Import 导入其他配置类 \u0026lt;import\u0026gt; @Scope(\u0026quot;prototype\u0026quot;) 作用范围 scope=\u0026quot;prototype\u0026quot; 2.2 Bean 生命周期回调 @Component public class CacheService { @PostConstruct // Bean 初始化完成（属性注入后）执行 public void init() { System.out.println(\u0026#34;缓存预热中...\u0026#34;); // 连接 Redis, 加载热点数据 } @PreDestroy // Bean 销毁前执行（容器关闭时） public void destroy() { System.out.println(\u0026#34;清理缓存连接...\u0026#34;); // 释放连接资源 } } ⚠️ @PreDestroy 在 prototype 作用域的 Bean 上不会触发，容器不追踪 prototype Bean 的生命周期。\n2.3 依赖注入注解 @Service public class UserService { // @Autowired：按类型注入（Type） // 如果同类型有多个Bean，再按字段名匹配 @Autowired private UserMapper userMapper; // @Qualifier：指定注入哪个 Bean（当同类型有多个时必须加） @Autowired @Qualifier(\u0026#34;mysqlUserMapper\u0026#34;) private UserMapper specificMapper; // @Value：注入简单值或 SpEL 表达式 @Value(\u0026#34;${app.timeout:5000}\u0026#34;) // 读取配置，默认值5000 private int timeout; @Value(\u0026#34;#{systemProperties[\u0026#39;os.name\u0026#39;]}\u0026#34;) // SpEL表达式 private String osName; } @Value 读取 .properties 文件的前提步骤：\n// 1. 在配置类上添加 @PropertySource @Configuration @ComponentScan(\u0026#34;com.example\u0026#34;) @PropertySource(\u0026#34;classpath:application.properties\u0026#34;) // 不支持通配符！ // 多文件写法： // @PropertySource({\u0026#34;classpath:db.properties\u0026#34;, \u0026#34;classpath:app.properties\u0026#34;}) public class SpringConfig { } // 2. 然后才能在 Bean 中使用 @Value 读取 @Value(\u0026#34;${jdbc.url}\u0026#34;) private String jdbcUrl; ⚠️ @PropertySource 不支持通配符（如 classpath*:*.properties），需要逐一列出文件路径。\n2.4 第三方 Bean 管理 推荐方式：@Configuration + @Import\n// 数据源配置类 @Configuration public class JdbcConfig { @Value(\u0026#34;${jdbc.url}\u0026#34;) private String url; @Value(\u0026#34;${jdbc.username}\u0026#34;) private String username; @Value(\u0026#34;${jdbc.password}\u0026#34;) private String password; // 引用类型注入：直接在方法参数上声明，Spring 自动注入 @Bean public DataSource dataSource() { DruidDataSource ds = new DruidDataSource(); ds.setUrl(url); ds.setUsername(username); ds.setPassword(password); return ds; } } // 主配置类：用 @Import 聚合，不用 @ComponentScan 扫描配置类 @Configuration @ComponentScan(\u0026#34;com.example\u0026#34;) @PropertySource(\u0026#34;classpath:application.properties\u0026#34;) @Import({JdbcConfig.class, MyBatisConfig.class}) // 推荐 public class SpringConfig { } 为什么推荐 @Import 而非扫描式？\n扫描式（让 @Configuration 类落在 @ComponentScan 包内）会让所有配置类都被\u0026quot;顺带\u0026quot;扫描进来，管理混乱。@Import 明确声明依赖关系，一目了然。\n2.5 整合 MyBatis 完整配置 @Configuration public class MyBatisConfig { // DataSource 会从 Spring 容器自动注入（方法参数注入） @Bean public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); factory.setDataSource(dataSource); // 设置 MyBatis 全局配置（可选） // factory.setConfigLocation(new ClassPathResource(\u0026#34;mybatis-config.xml\u0026#34;)); // 开启驼峰命名转换 org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration(); config.setMapUnderscoreToCamelCase(true); factory.setConfiguration(config); return factory; } @Bean public MapperScannerConfigurer mapperScannerConfigurer() { MapperScannerConfigurer msc = new MapperScannerConfigurer(); // 扫描 Mapper 接口所在包，自动生成代理对象注册到容器 msc.setBasePackage(\u0026#34;com.example.mapper\u0026#34;); return msc; } } 2.6 整合 JUnit 5（现代写法） // JUnit 4 写法（老项目） @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = SpringConfig.class) public class UserServiceTest { @Autowired private UserService userService; @Test public void testFind() { /* ... */ } } // JUnit 5 写法（推荐，SpringBoot 默认） @SpringJUnitConfig(classes = SpringConfig.class) // 等价于 @ExtendWith(SpringExtension.class) + @ContextConfiguration public class UserServiceTest { @Autowired private UserService userService; @Test void testFind() { /* ... */ } } 三、Spring AOP 3.1 核心概念 连接点（JoinPoint） → 程序执行的\u0026#34;任意可能位置\u0026#34;（所有方法都是连接点） 切入点（Pointcut） → 实际被拦截的方法（连接点的子集） 通知（Advice） → 切入点位置要执行的增强逻辑 切面（Aspect） → 切入点 + 通知的组合（通知类） 织入（Weaving） → 将切面应用到目标对象的过程 类比 Python 装饰器： @around_advice ← 通知 def save_user(): ← 切入点（被装饰的函数） pass 连接点 vs 切入点区别：\n连接点是\u0026quot;理论上可以拦截的所有方法\u0026quot;，切入点是\u0026quot;你实际配置要拦截的方法\u0026quot;。所有切入点都是连接点，但连接点不一定是切入点。\n3.2 切入点表达式 // 语法：execution(访问修饰符? 返回值类型 包名.类名?.方法名(参数) 异常?) // ? 表示可省略 // 精确匹配 @Pointcut(\u0026#34;execution(void com.example.service.UserService.save())\u0026#34;) // 匹配某个类的所有方法 @Pointcut(\u0026#34;execution(* com.example.service.UserService.*(..))\u0026#34;) // ^返回值任意 ^方法名任意 ^参数任意 // 匹配某个包下所有类的所有方法（不含子包） @Pointcut(\u0026#34;execution(* com.example.service.*.*(..))\u0026#34;) // 匹配某个包及子包下所有类的所有方法 @Pointcut(\u0026#34;execution(* com.example.service..*.*(..))\u0026#34;) // ^^ 双点表示含子包 // 实际项目常用写法：拦截 service 层所有方法 @Pointcut(\u0026#34;execution(* com.example.service.impl.*.*(..))\u0026#34;) 3.3 通知类型完整示例 @Aspect @Component public class LogAspect { // 定义可复用的切入点 @Pointcut(\u0026#34;execution(* com.example.service..*.*(..))\u0026#34;) public void serviceMethods() {} // 前置通知：方法执行前 @Before(\u0026#34;serviceMethods()\u0026#34;) public void before(JoinPoint jp) { System.out.println(\u0026#34;方法开始：\u0026#34; + jp.getSignature().getName()); Object[] args = jp.getArgs(); // 获取参数 System.out.println(\u0026#34;参数：\u0026#34; + Arrays.toString(args)); } // 后置通知：方法执行后（无论是否异常） @After(\u0026#34;serviceMethods()\u0026#34;) public void after(JoinPoint jp) { System.out.println(\u0026#34;方法结束：\u0026#34; + jp.getSignature().getName()); } // 返回后通知：方法正常返回后（有异常不执行） @AfterReturning(value = \u0026#34;serviceMethods()\u0026#34;, returning = \u0026#34;result\u0026#34;) public void afterReturning(JoinPoint jp, Object result) { System.out.println(\u0026#34;返回值：\u0026#34; + result); } // 异常通知：方法抛出异常后 @AfterThrowing(value = \u0026#34;serviceMethods()\u0026#34;, throwing = \u0026#34;ex\u0026#34;) public void afterThrowing(JoinPoint jp, Exception ex) { System.out.println(\u0026#34;异常：\u0026#34; + ex.getMessage()); // 可在此发送告警通知 } // 环绕通知：最强大，可控制是否执行原方法 @Around(\u0026#34;serviceMethods()\u0026#34;) public Object around(ProceedingJoinPoint pjp) throws Throwable { // 获取方法签名 String methodName = pjp.getSignature().getName(); Object[] args = pjp.getArgs(); long start = System.currentTimeMillis(); try { // 执行原方法（不调用则原方法不执行） Object result = pjp.proceed(); long cost = System.currentTimeMillis() - start; System.out.printf(\u0026#34;[%s] 耗时: %dms%n\u0026#34;, methodName, cost); return result; } catch (Throwable e) { System.out.println(\u0026#34;[\u0026#34; + methodName + \u0026#34;] 异常: \u0026#34; + e.getMessage()); throw e; // 一定要重新抛出，否则异常被吞掉 } } } 通知执行顺序（正常情况）：\nAround(前) → Before → 目标方法 → Around(后) → AfterReturning → After 通知执行顺序（异常情况）：\nAround(前) → Before → 目标方法(抛异常) → AfterThrowing → After 3.4 AOP 底层代理机制 // Spring AOP 两种代理方式： // 1. JDK 动态代理：目标类实现了接口 → 代理接口 // 2. CGLIB 代理：目标类没有接口 → 继承目标类生成子类 // Spring Boot 默认使用 CGLIB（无论是否有接口） // 原因：避免\u0026#34;必须面向接口\u0026#34;的约束，使用更灵活 // CGLIB 注意： // final 类/方法无法被 CGLIB 代理（无法继承/重写） 四、Spring 事务 4.1 基本使用 // 1. 配置事务管理器（整合MyBatis时用DataSourceTransactionManager） @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } // 2. 开启事务注解支持 @Configuration @EnableTransactionManagement // 开启 @Transactional 注解支持 public class SpringConfig { } // 3. 在 Service 方法上使用 @Service public class AccountService { // 写在接口上：降低耦合（推荐），实现类自动继承 // 写在实现类上：更明确，但耦合接口实现 @Transactional public void transfer(Long fromId, Long toId, BigDecimal amount) { accountMapper.deduct(fromId, amount); // 如果这里抛出异常，上面的 deduct 会自动回滚 accountMapper.add(toId, amount); } } 4.2 事务传播行为 场景：A 方法（事务管理员）调用 B 方法（事务协调员）\n传播行为 说明 场景 REQUIRED（默认） 有事务就加入，没有就新建 99% 场景 REQUIRES_NEW 无论如何都新建独立事务，与调用方事务隔离 日志记录（主流程回滚，日志仍要保存） SUPPORTS 有事务就加入，没有就不用事务执行 只读查询 NOT_SUPPORTED 不使用事务（挂起当前事务） 不需要事务的操作 MANDATORY 必须在已有事务中执行，否则抛异常 强制要求调用方开启事务 NEVER 不能在事务中执行，否则抛异常 - NESTED 在当前事务中创建保存点（嵌套事务） 部分回滚 @Service public class OrderService { @Autowired private LogService logService; @Transactional public void createOrder(Order order) { orderMapper.insert(order); // 即使 createOrder 最终回滚，日志仍然写入成功 logService.writeLog(\u0026#34;创建订单: \u0026#34; + order.getId()); // 模拟异常触发回滚 if (order.getAmount().compareTo(BigDecimal.ZERO) \u0026lt; 0) { throw new BusinessException(\u0026#34;金额不能为负\u0026#34;); } } } @Service public class LogService { // REQUIRES_NEW：开启独立事务，不受外层事务影响 @Transactional(propagation = Propagation.REQUIRES_NEW) public void writeLog(String message) { logMapper.insert(new Log(message, LocalDateTime.now())); } } 4.3 @Transactional 注意事项 // ❌ 错误：方法不是 public 的，@Transactional 不生效 @Transactional private void internalTransfer() { } // ❌ 错误：同一类内部调用，绕过代理，事务不生效 @Service public class UserService { public void doA() { this.doB(); // 直接调用，不经过 AOP 代理！ } @Transactional public void doB() { } } // ✅ 正确：注入自身代理（或通过 AopContext.currentProxy()） @Service public class UserService { @Autowired private UserService self; // 注入代理对象 public void doA() { self.doB(); // 通过代理调用，事务生效 } } // ❌ 错误：异常被 catch 了，Spring 不知道要回滚 @Transactional public void save(User user) { try { userMapper.insert(user); } catch (Exception e) { log.error(\u0026#34;保存失败\u0026#34;, e); // 没有 rethrow，事务不会回滚！ } } // ✅ 正确：或者手动标记回滚 @Transactional public void save(User user) { try { userMapper.insert(user); } catch (Exception e) { log.error(\u0026#34;保存失败\u0026#34;, e); TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } } ⚠️ @Transactional 默认只回滚 RuntimeException 和 Error，受检异常（IOException 等）不触发回滚。 需要回滚受检异常：@Transactional(rollbackFor = Exception.class)\n五、SpringMVC 5.1 基本工作流程 HTTP请求 ↓ DispatcherServlet（前端控制器，核心） ↓ HandlerMapping（根据URL找到对应的Controller方法） ↓ HandlerAdapter（调用Controller方法，处理参数绑定） ↓ Controller 方法执行 → 返回 ModelAndView 或 @ResponseBody 数据 ↓ ViewResolver（解析视图名，@ResponseBody 跳过此步） ↓ 渲染响应 → 返回给客户端 5.2 请求参数接收 @RestController // = @Controller + @ResponseBody @RequestMapping(\u0026#34;/users\u0026#34;) public class UserController { // 1. 普通参数：名称匹配自动绑定 @GetMapping public List\u0026lt;User\u0026gt; list(String name, Integer age) { } // 2. POJO 参数：自动绑定同名字段 @PostMapping public User create(UserCreateDTO dto) { } // 3. 数组参数：同名多个值 @GetMapping(\u0026#34;/batch\u0026#34;) public List\u0026lt;User\u0026gt; batch(String[] ids) { } // 4. 集合参数：必须加 @RequestParam @GetMapping(\u0026#34;/list\u0026#34;) public List\u0026lt;User\u0026gt; list(@RequestParam List\u0026lt;String\u0026gt; ids) { } // 5. JSON 参数：必须加 @RequestBody，需要 jackson-databind @PostMapping(\u0026#34;/json\u0026#34;) public Result createFromJson(@RequestBody UserCreateDTO dto) { } // 6. 路径参数 @GetMapping(\u0026#34;/{id}\u0026#34;) public User getById(@PathVariable Long id) { } // 7. 日期参数：指定格式 @GetMapping(\u0026#34;/born\u0026#34;) public List\u0026lt;User\u0026gt; byBirth( @DateTimeFormat(pattern = \u0026#34;yyyy-MM-dd\u0026#34;) LocalDate birthDate) { } } 三个参数注解的区别：\n注解 取值来源 典型场景 @RequestParam URL 查询参数（?key=value） 分页参数、过滤条件 @RequestBody 请求体（JSON/XML） POST/PUT 提交数据 @PathVariable URL 路径段（/users/{id}） RESTful 资源ID 5.3 表现层统一响应封装 // 统一响应体 @Data @AllArgsConstructor @NoArgsConstructor public class Result\u0026lt;T\u0026gt; { private Integer code; // 业务状态码 private String message; private T data; public static \u0026lt;T\u0026gt; Result\u0026lt;T\u0026gt; success(T data) { return new Result\u0026lt;\u0026gt;(200, \u0026#34;success\u0026#34;, data); } public static \u0026lt;T\u0026gt; Result\u0026lt;T\u0026gt; fail(Integer code, String message) { return new Result\u0026lt;\u0026gt;(code, message, null); } } // Controller 使用 @GetMapping(\u0026#34;/{id}\u0026#34;) public Result\u0026lt;User\u0026gt; getById(@PathVariable Long id) { User user = userService.getById(id); return Result.success(user); } 5.4 全局异常处理 // 自定义业务异常 public class BusinessException extends RuntimeException { private final Integer code; public BusinessException(Integer code, String message) { super(message); this.code = code; } } // 自定义系统异常 public class SystemException extends RuntimeException { private final Integer code; public SystemException(Integer code, String message, Throwable cause) { super(message, cause); this.code = code; } } // 全局异常处理器 @RestControllerAdvice // = @ControllerAdvice + @ResponseBody public class GlobalExceptionHandler { // 处理业务异常（预期内的，如参数校验失败、业务规则冲突） @ExceptionHandler(BusinessException.class) public Result\u0026lt;Void\u0026gt; handleBusiness(BusinessException e) { log.warn(\u0026#34;业务异常: {}\u0026#34;, e.getMessage()); return Result.fail(e.getCode(), e.getMessage()); } // 处理系统异常（预期外的，如数据库连接失败） @ExceptionHandler(SystemException.class) public Result\u0026lt;Void\u0026gt; handleSystem(SystemException e) { log.error(\u0026#34;系统异常: \u0026#34;, e); return Result.fail(500, \u0026#34;系统繁忙，请稍后重试\u0026#34;); } // 处理所有未捕获异常（兜底） @ExceptionHandler(Exception.class) public Result\u0026lt;Void\u0026gt; handleAll(Exception e) { log.error(\u0026#34;未知异常: \u0026#34;, e); return Result.fail(500, \u0026#34;服务器内部错误\u0026#34;); } } 5.5 拦截器（Interceptor） 拦截器 vs 过滤器 对比项 拦截器（Interceptor） 过滤器（Filter） 规范 Spring MVC Servlet 作用范围 Controller 请求 所有请求（包括静态资源） 访问 Spring Bean ✅ 可以 ❌ 较难 粒度 更细（方法级别） 较粗（URL级别） 典型用途 登录检查、权限校验、日志 编码处理、跨域、限流 拦截器实现 // 1. 实现 HandlerInterceptor 接口 @Component public class LoginInterceptor implements HandlerInterceptor { @Autowired private JwtUtil jwtUtil; // 请求处理前：返回 false 则中断请求链 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader(\u0026#34;Authorization\u0026#34;); if (token == null || !jwtUtil.verify(token)) { response.setStatus(401); response.getWriter().write(\u0026#34;{\\\u0026#34;code\\\u0026#34;:401,\\\u0026#34;message\\\u0026#34;:\\\u0026#34;请先登录\\\u0026#34;}\u0026#34;); return false; // 中断，不再继续 } return true; // 放行 } // 请求处理后（Controller执行后）：可修改ModelAndView @Override public void postHandle(HttpServletRequest req, HttpServletResponse res, Object handler, ModelAndView mv) { } // 视图渲染完成后：用于资源清理 @Override public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object handler, Exception ex) { // 清理 ThreadLocal 等资源 } } // 2. 注册拦截器 @Configuration public class WebConfig extends WebMvcConfigurationSupport { @Autowired private LoginInterceptor loginInterceptor; @Override protected void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(\u0026#34;/**\u0026#34;) // 拦截所有 .excludePathPatterns( // 放行 \u0026#34;/users/login\u0026#34;, \u0026#34;/users/register\u0026#34;, \u0026#34;/swagger-ui/**\u0026#34; ); } } 多拦截器执行顺序 注册顺序：Interceptor1 → Interceptor2 → Interceptor3 正常执行： pre1 → pre2 → pre3 → [Controller] → post3 → post2 → post1 → after3 → after2 → after1 pre3 返回 false（pre3 之前的已执行）： pre1 → pre2 → pre3(false) → after2 → after1 （注意：after 只执行 preHandle 返回 true 的拦截器） pre2 返回 false： pre1 → pre2(false) → after1 pre1 返回 false： pre1(false) → （什么都不执行） 六、Maven 工程管理 6.1 依赖冲突解决规则 优先级（从高到低）： 1. 特殊优先：同一 pom.xml 中配置了相同依赖不同版本 → 后声明的覆盖先声明的 2. 路径优先：依赖传递路径短的优先 A → B → C → log4j:1.1 （路径长度3） A → D → log4j:1.2 （路径长度2） ← 优先使用 1.2 3. 声明优先：路径相同时，pom.xml 中先声明的 dependency 优先 主动解决冲突：\n\u0026lt;!-- 方法1：排除传递依赖 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-core\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;5.3.20\u0026lt;/version\u0026gt; \u0026lt;exclusions\u0026gt; \u0026lt;exclusion\u0026gt; \u0026lt;!-- 排除 spring-core 带来的旧版 commons-logging --\u0026gt; \u0026lt;groupId\u0026gt;commons-logging\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;commons-logging\u0026lt;/artifactId\u0026gt; \u0026lt;/exclusion\u0026gt; \u0026lt;/exclusions\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;!-- 方法2：在父pom统一锁定版本（dependencyManagement） --\u0026gt; \u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.alibaba\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;druid\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.2.15\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; 6.2 依赖范围（scope） scope 编译 测试 运行时 说明 compile（默认） ✅ ✅ ✅ 全范围，会打包进jar/war test ❌ ✅ ❌ 仅测试，如 JUnit provided ✅ ✅ ❌ 运行时由容器提供，如 servlet-api runtime ❌ ✅ ✅ 只运行时需要，如 JDBC 驱动 javax.servlet-api 为什么必须用 provided？\nTomcat 容器本身已经包含了 Servlet API 的实现。如果打包进 war，会与 Tomcat 自带的版本冲突，导致 ClassCastException 或类加载错误。provided 表示\u0026quot;编译时需要，运行时由容器提供，打包时排除\u0026quot;。\n6.3 继承与聚合 \u0026lt;!-- 父工程 pom.xml（packaging 必须是 pom） --\u0026gt; \u0026lt;groupId\u0026gt;com.example\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.0\u0026lt;/version\u0026gt; \u0026lt;packaging\u0026gt;pom\u0026lt;/packaging\u0026gt; \u0026lt;!-- 聚合：父工程管理多个子模块（mvn package 自动按序构建所有模块） --\u0026gt; \u0026lt;modules\u0026gt; \u0026lt;module\u0026gt;common\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;service\u0026lt;/module\u0026gt; \u0026lt;module\u0026gt;web\u0026lt;/module\u0026gt; \u0026lt;/modules\u0026gt; \u0026lt;!-- 依赖管理：声明版本，子模块继承时不必写版本号 --\u0026gt; \u0026lt;dependencyManagement\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-dependencies\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.7.0\u0026lt;/version\u0026gt; \u0026lt;type\u0026gt;pom\u0026lt;/type\u0026gt; \u0026lt;scope\u0026gt;import\u0026lt;/scope\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; \u0026lt;/dependencyManagement\u0026gt; \u0026lt;!-- 子工程 pom.xml --\u0026gt; \u0026lt;parent\u0026gt; \u0026lt;groupId\u0026gt;com.example\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;parent\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;1.0.0\u0026lt;/version\u0026gt; \u0026lt;relativePath\u0026gt;../parent/pom.xml\u0026lt;/relativePath\u0026gt; \u0026lt;/parent\u0026gt; \u0026lt;artifactId\u0026gt;service\u0026lt;/artifactId\u0026gt; \u0026lt;!-- 无需写 groupId 和 version，继承自父工程 --\u0026gt; \u0026lt;dependencies\u0026gt; \u0026lt;!-- 无需写版本，由父工程 dependencyManagement 管理 --\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.springframework.boot\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;spring-boot-starter-web\u0026lt;/artifactId\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;/dependencies\u0026gt; 6.4 多环境配置 \u0026lt;!-- pom.xml 中定义 profiles --\u0026gt; \u0026lt;profiles\u0026gt; \u0026lt;profile\u0026gt; \u0026lt;id\u0026gt;dev\u0026lt;/id\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;profile.active\u0026gt;dev\u0026lt;/profile.active\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;activation\u0026gt; \u0026lt;activeByDefault\u0026gt;true\u0026lt;/activeByDefault\u0026gt; \u0026lt;!-- 默认激活 --\u0026gt; \u0026lt;/activation\u0026gt; \u0026lt;/profile\u0026gt; \u0026lt;profile\u0026gt; \u0026lt;id\u0026gt;prod\u0026lt;/id\u0026gt; \u0026lt;properties\u0026gt; \u0026lt;profile.active\u0026gt;prod\u0026lt;/profile.active\u0026gt; \u0026lt;/properties\u0026gt; \u0026lt;/profile\u0026gt; \u0026lt;/profiles\u0026gt; # 构建时指定环境 mvn package -P prod # 跳过测试 mvn package -DskipTests # 或 mvn package -Dmaven.test.skip=true # 连编译测试代码都跳过 七、SSM 整合原理深挖 7.1 Spring 双容器架构（重点） Tomcat 启动 ↓ 发现 AbstractAnnotationConfigDispatcherServletInitializer ↓ 创建两个 Spring 容器（父子关系）： ┌─────────────────────────────────────────────────────┐ │ Root ApplicationContext（父容器） │ │ 读取 SpringConfig │ │ 包含：Service, Mapper代理, DataSource, 事务管理器 │ │ 职责：业务逻辑 + 数据访问层 │ └─────────────────────────────────────────────────────┘ ↑（子容器可以访问父容器，反之不行） ┌─────────────────────────────────────────────────────┐ │ Servlet ApplicationContext（子容器/MVC容器） │ │ 读取 SpringMvcConfig │ │ 包含：Controller, ViewResolver, HandlerMapping │ │ 职责：接收 HTTP 请求，分发处理 │ └─────────────────────────────────────────────────────┘ 为什么这样设计？\n层级隔离：Controller 可以注入 Service（子访问父），但 Service 不能注入 Controller（防止业务层与表现层耦合）。也支持不同协议共享同一个业务容器（如同时支持 HTTP + WebSocket）。\n7.2 Bean 加载控制（防止重复扫描） // SpringConfig：只扫描非 Controller 的组件 @Configuration @ComponentScan(value = \u0026#34;com.example\u0026#34;, excludeFilters = @ComponentScan.Filter( type = FilterType.ANNOTATION, classes = Controller.class ) ) public class SpringConfig { } // SpringMvcConfig：只扫描 Controller @Configuration @ComponentScan(\u0026#34;com.example.controller\u0026#34;) @EnableWebMvc public class SpringMvcConfig { } ⚠️ 如果两个配置都扫描了相同的包，Service 会被创建两次（父容器一次，子容器一次），导致事务等 AOP 增强失效（因为子容器的 Service 没有经过父容器的事务代理）。\n7.3 @EnableWebMvc 做了什么 // @EnableWebMvc 本质： // @Import(DelegatingWebMvcConfiguration.class) // 它向容器注册了一套完整的 MVC 组件： // 启用 @RequestMapping 路由映射 // 启用 @Controller / @ResponseBody 注解处理 // 启用 JSON 自动转换（需要 jackson-databind 在 classpath） // 注册参数绑定转换器（@DateTimeFormat 等） // 注册 DispatcherServlet 相关组件（HandlerAdapter 等） // 启用静态资源处理、视图解析等 7.4 WebMvcConfigurer vs WebMvcConfigurationSupport // 方式1：实现 WebMvcConfigurer（推荐，非侵入式） @Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { } @Override public void addCorsMappings(CorsRegistry registry) { } // 只覆盖需要的方法，其他保持 @EnableWebMvc 默认配置 } // 方式2：继承 WebMvcConfigurationSupport（侵入式，慎用） @Configuration public class WebConfig extends WebMvcConfigurationSupport { // 继承此类后，@EnableWebMvc 的自动配置会被禁用！ // 必须手动配置所有需要的 MVC 组件，否则很多默认功能失效 } 为什么 WebMvcConfigurationSupport 有侵入性？\n继承它意味着接管了整个 MVC 配置，@EnableWebMvc 失效，Spring Boot 的 WebMvcAutoConfiguration 也失效。如果不小心遗漏某些配置，会导致 JSON 转换不工作、静态资源 404 等问题。\n八、高频 QA 答疑 Q1：Spring 和 Spring Boot 的版本关系？ Spring Boot 3.x → Spring Framework 6.x（要求 JDK 17+） Spring Boot 2.x → Spring Framework 5.x（支持 JDK 8+） Spring Boot 1.x → Spring Framework 4.x 查询对应关系：https://spring.io/projects/spring-boot#learn （Release Notes 中有明确说明） Q2：AOP 切入点（Pointcut）和连接点（JoinPoint）的区别？ 连接点（JoinPoint）= 候选人（所有方法执行点） 切入点（Pointcut）= 选中的候选人（通过表达式过滤后实际拦截的方法） 例：一个 Service 类有 100 个方法（100个连接点） 你配置 execution(* com.example.service.*.save*(..)) → 只有 save 开头的 5 个方法是切入点 Q3：spring-jdbc 和 mybatis-spring 如何配合？ spring-jdbc 提供： - DataSourceTransactionManager（事务管理器） - JdbcTemplate（可选） - 整合事务的基础设施 mybatis-spring 提供： - SqlSessionFactoryBean（创建 MyBatis 核心对象并注册到 Spring） - MapperScannerConfigurer（扫描 Mapper 接口，生成代理 Bean） - SqlSessionTemplate（线程安全的 SqlSession） 关系：mybatis-spring 把 MyBatis 的 DataSource 指向 Spring 管理的 DataSource， 使 MyBatis 的操作参与到 Spring 的事务管理中。 Q4：编译时和运行时的区别？ 编译时：javac 将 .java → .class 的过程 - 语法检查 - 类型检查 - 注解处理（如 Lombok 在编译时生成代码） - 编译期异常（受检异常 Checked Exception） 运行时：JVM 加载并执行 .class 的过程 - 动态代理（Spring AOP 在运行时生成代理类） - 反射 - 类加载 - 运行时异常（RuntimeException 及其子类） Q5：default 和 protected 的区别？ // default（包访问权限，不写修饰符） class DefaultClass { void defaultMethod() {} // 只有同包的类可以访问 } // protected class Base { protected void protectedMethod() {} } // 关键区别：不同包的子类 // ✅ protected 可以跨包继承 package com.other; import com.example.Base; class Sub extends Base { void test() { protectedMethod(); // ✅ 可以访问 } } // ❌ default 不能跨包继承 package com.other; import com.example.DefaultClass; class Sub extends DefaultClass { void test() { defaultMethod(); // ❌ 编译错误：不可见 } } // 记忆： // 同包内：default 和 protected 都可以访问 // 不同包子类：只有 protected 可以访问（default 不行） // 不同包非子类：default 和 protected 都不可以访问 Q6：@Value 和直接赋值的区别？ // 直接赋值：硬编码，无法外部配置 private int timeout = 5000; // @Value：运行时从配置文件/环境变量读取，支持外部化配置 @Value(\u0026#34;${app.timeout:5000}\u0026#34;) // 默认值5000 private int timeout; // @Value 的其他用法： @Value(\u0026#34;${server.port}\u0026#34;) // 读取application.properties @Value(\u0026#34;#{T(Math).PI}\u0026#34;) // SpEL表达式 @Value(\u0026#34;#{systemProperties[\u0026#39;java.home\u0026#39;]}\u0026#34;) // 系统属性 @Value(\u0026#34;${MY_ENV_VAR}\u0026#34;) // 环境变量 ","permalink":"https://lv-blog.pages.dev/posts/programming/backend/ssm/","summary":"\u003ch2 id=\"一spring-ioc-容器\"\u003e一、Spring IoC 容器\u003c/h2\u003e\n\u003ch3 id=\"11-容器体系结构\"\u003e1.1 容器体系结构\u003c/h3\u003e\n\u003cpre tabindex=\"0\"\u003e\u003ccode\u003eBeanFactory（顶层接口）\n    └── ApplicationContext（常用接口，扩展了BF）\n            ├── ClassPathXmlApplicationContext（XML配置）\n            ├── FileSystemXmlApplicationContext（文件路径XML）\n            └── AnnotationConfigApplicationContext（注解配置）\n\u003c/code\u003e\u003c/pre\u003e\u003cp\u003e\u003cstrong\u003eBeanFactory vs ApplicationContext 核心区别：\u003c/strong\u003e\u003c/p\u003e\n\u003ctable\u003e\n\t\u003cthead\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003cth\u003e特性\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eBeanFactory\u003c/th\u003e\n\t\t\t\t\t\u003cth\u003eApplicationContext\u003c/th\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/thead\u003e\n\t\u003ctbody\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003eBean 初始化时机\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e懒加载\u003c/strong\u003e（第一次 getBean 时）\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e\u003cstrong\u003e饿加载\u003c/strong\u003e（容器启动时）\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e功能\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e基础 IoC\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003eIoC + 事件发布 + 国际化 + AOP等\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\t\t\u003ctr\u003e\n\t\t\t\t\t\u003ctd\u003e使用场景\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e资源极度受限的嵌入式\u003c/td\u003e\n\t\t\t\t\t\u003ctd\u003e99% 的业务场景\u003c/td\u003e\n\t\t\t\u003c/tr\u003e\n\t\u003c/tbody\u003e\n\u003c/table\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eApplicationContext 为什么没有 close()？\u003c/strong\u003e\u003cbr\u003e\n\u003ccode\u003eApplicationContext\u003c/code\u003e 接口本身不定义 \u003ccode\u003eclose()\u003c/code\u003e，是为了保持接口的通用性（不是所有容器都能/需要被关闭，如 Web 容器）。但其实现类 \u003ccode\u003eAbstractApplicationContext\u003c/code\u003e 实现了 \u003ccode\u003eCloseable\u003c/code\u003e，可以强转后调用，或用 \u003ccode\u003eConfigurableApplicationContext\u003c/code\u003e 接口接收。\u003c/p\u003e\n\u003c/blockquote\u003e\n\u003chr\u003e\n\u003ch3 id=\"12-延迟加载lazy-loading\"\u003e1.2 延迟加载（Lazy Loading）\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// 注解方式：@Lazy 让 Bean 在第一次被使用时才初始化\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e@Bean\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e@Lazy\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e HeavyService \u003cspan style=\"color:#a6e22e\"\u003eheavyService\u003c/span\u003e() {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003ereturn\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003enew\u003c/span\u003e HeavyService();\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// XML方式：\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#75715e\"\u003e// \u0026lt;bean id=\u0026#34;heavyService\u0026#34; class=\u0026#34;...\u0026#34; lazy-init=\u0026#34;true\u0026#34;/\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e适用场景\u003c/strong\u003e：初始化代价高、启动时不一定用到的 Bean（如某些连接池、第三方SDK客户端）。\u003c/p\u003e","title":"SSM框架 学习"},{"content":"1. 简介 MyBatis-Plus（简称 MP）是一个 MyBatis 的增强工具，在 MyBatis 的基础上只做增强不做改变，为简化开发、提高效率而生。\n核心特点：\n无侵入：引入 MP 不会对现有 MyBatis 工程产生影响，犹如丝般顺滑。 损耗小：启动即会自动注入基本 CRUD，性能基本无损耗。 强大的 CRUD 操作：内置通用 Mapper、通用 Service，仅通过少量配置即可实现单表大部分 CRUD 操作。 支持 Lambda 形式调用：通过 Lambda 表达式，安全高效的编写查询条件，防止字段名误写。 内置代码生成器：通过少量配置即可生成 Mapper、Service、Controller 等代码。 内置分页插件：基于 MyBatis 物理分页，开发者无需关心具体操作，配置后即可使用。 官网：https://baomidou.com/\n2. 基本用法 2.1 引入依赖 在 Maven 项目 pom.xml 中添加：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.baomidou\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis-plus-boot-starter\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.3.1\u0026lt;/version\u0026gt; \u0026lt;!-- 请按最新版本 --\u0026gt; \u0026lt;/dependency\u0026gt; 如果是传统 Spring 项目，可引入 mybatis-plus 核心依赖并自行配置。通常 Spring Boot 项目直接使用 starter。\n2.2 定义实体类 @Data public class User { private Long id; private String name; private Integer age; private String email; } 2.3 编写 Mapper 接口 继承 BaseMapper\u0026lt;T\u0026gt; 即可获得 CRUD 能力：\nimport com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface UserMapper extends BaseMapper\u0026lt;User\u0026gt; { } 此时 UserMapper 已经拥有了 insert、deleteById、updateById、selectById、selectList 等大量方法，无需编写 XML。\n2.4 扫描 Mapper Spring Boot 启动类需添加 @MapperScan 扫描 Mapper 包：\n@SpringBootApplication @MapperScan(\u0026#34;com.example.mapper\u0026#34;) public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } 3. 常用注解 3.1 @TableName 作用：当表名与实体类名不一致时，指定映射的表名。 属性： value：表名 schema：模式 keepGlobalPrefix：是否保持全局表前缀（默认 false） autoResultMap：是否自动构建 ResultMap（配合 typeHandler 使用，如 JSON 处理器） @TableName(\u0026#34;t_user\u0026#34;) public class User { ... } 3.2 @TableId 作用：标识主键字段。 属性： value：主键字段名（若与属性名不一致） type：主键生成策略，使用 IdType 枚举，常用如下： AUTO：数据库自增，需数据库支持自增主键。 INPUT：用户自定义输入。 ASSIGN_ID（默认）：雪花算法生成 Long 型 ID。 ASSIGN_UUID：以 UUID 形式生成（去除中划线）。 public class User { @TableId(value = \u0026#34;user_id\u0026#34;, type = IdType.AUTO) private Long id; // ... } 注意：若实体类主键属性名就叫 id 且表主键列也为 id，可不加此注解；默认主键策略为 ASSIGN_ID。\n3.3 @TableField 作用：标识非主键的普通字段。 属性： value：指定数据库列名（当字段名与列名不一致时）。 exist：是否为数据库字段（false 表示不是，该字段不会参与 SQL 生成）。 condition：自定义查询条件规则（一般不常用）。 fill：字段自动填充策略（配合 MetaObjectHandler）。 select：是否进行 select 查询（默认 true）。 typeHandler：指定类型处理器（如 JSON 处理）。 常见使用场景：\n属性名以 is 开头的布尔字段\n若实体中有 Boolean isAdult，MP 默认会自动去掉 is 前缀寻找数据库 adult 列，此时需手动指定列名：\n@TableField(\u0026#34;is_adult\u0026#34;) private Boolean isAdult; 数据库关键字冲突\n比如实体属性为 order，与 MySQL 关键字冲突，可加 value 反引号或改名：\n@TableField(\u0026#34;`order`\u0026#34;) private Integer order; 数据库不存在的字段\n比如用于业务逻辑的临时字段：\n@TableField(exist = false) private String remark; // 该字段不会保存到数据库 4. 默认约定 MP 遵循“约定优于配置”原则：\n默认类名转下划线作为表名（如 UserInfo → user_info）。 主键策略默认为 ASSIGN_ID（雪花算法）。 字段名转下划线映射列名（如 createTime → create_time）。 若属性名为 id，且类型为 Long，会自动识别为主键。 这些约定可以通过配置或注解覆盖。 5. 常见配置 在 application.yml 中进行常用配置：\nmybatis-plus: # 类型别名包扫描，通常配了这个即可，无需单独配置 type-aliases-package 在 mybatis 下 type-aliases-package: com.example.entity # Mapper XML 文件位置（多目录使用逗号分隔） mapper-locations: classpath*:/mapper/**/*.xml # MyBatis 原生配置 configuration: # 下划线转驼峰（默认开启） map-underscore-to-camel-case: true # 缓存开启（默认开启） cache-enabled: true # 全局配置 global-config: db-config: # 全局主键策略（优先级：注解 \u0026gt; 全局配置） id-type: assign_id # 全局字段更新策略（not_null：只更新非空字段；ignored：所有字段） update-strategy: not_null # 逻辑删除配置（后面细讲） logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 全局配置优先级低于注解：例如实体类中用 @TableId(type = IdType.AUTO) 指明了自增，即使全局配置了 assign_id 也会以注解为准。\n6. 核心功能 6.1 条件构造器 条件构造器是 MP 中最强大的功能之一，用于动态生成 WHERE 条件。主要有：\nQueryWrapper\u0026lt;T\u0026gt;：查询条件构造，可返回实体对象。 UpdateWrapper\u0026lt;T\u0026gt;：更新条件构造，可设置 set 子句。 LambdaQueryWrapper\u0026lt;T\u0026gt; / LambdaUpdateWrapper\u0026lt;T\u0026gt;：通过 Lambda 表达式引用字段，防止硬编码字符串出错。 常用方法举例：\n// 普通 QueryWrapper 示例 QueryWrapper\u0026lt;User\u0026gt; wrapper = new QueryWrapper\u0026lt;\u0026gt;(); wrapper.select(\u0026#34;id\u0026#34;, \u0026#34;name\u0026#34;) // 指定查询列 .like(\u0026#34;name\u0026#34;, \u0026#34;张\u0026#34;) // name like \u0026#39;%张%\u0026#39; .between(\u0026#34;age\u0026#34;, 18, 30) // age between 18 and 30 .eq(\u0026#34;email\u0026#34;, \u0026#34;test@baomidou.com\u0026#34;) // email = ? .orderByDesc(\u0026#34;id\u0026#34;); // order by id desc List\u0026lt;User\u0026gt; users = userMapper.selectList(wrapper); // Lambda 形式，更安全 LambdaQueryWrapper\u0026lt;User\u0026gt; lambdaQ = new LambdaQueryWrapper\u0026lt;\u0026gt;(); lambdaQ.like(User::getName, \u0026#34;张\u0026#34;) .between(User::getAge, 18, 30); List\u0026lt;User\u0026gt; lambdaUsers = userMapper.selectList(lambdaQ); // 更新部分字段 LambdaUpdateWrapper\u0026lt;User\u0026gt; lambdaU = new LambdaUpdateWrapper\u0026lt;\u0026gt;(); lambdaU.eq(User::getId, 1) .set(User::getAge, 25) .set(User::getEmail, \u0026#34;new@email.com\u0026#34;); userMapper.update(null, lambdaU); // 第一个参数为实体，可为 null 6.2 自定义 SQL 当需要编写复杂的 SQL，但又想利用 MP 的条件构造器动态拼接 WHERE 条件时，可以结合自定义 XML 和 ${ew.customSqlSegment}。\nMapper 接口：\n@Mapper public interface UserMapper extends BaseMapper\u0026lt;User\u0026gt; { // 参数必须用 @Param(\u0026#34;ew\u0026#34;) 接收 Wrapper，第二个参数可自定义 List\u0026lt;User\u0026gt; selectByMyWhere(@Param(\u0026#34;ew\u0026#34;) Wrapper\u0026lt;User\u0026gt; wrapper, @Param(\u0026#34;minAge\u0026#34;) Integer minAge); } XML 映射文件：\n\u0026lt;select id=\u0026#34;selectByMyWhere\u0026#34; resultType=\u0026#34;com.example.entity.User\u0026#34;\u0026gt; SELECT * FROM user ${ew.customSqlSegment} AND age \u0026gt; #{minAge} \u0026lt;/select\u0026gt; 调用时：\nLambdaQueryWrapper\u0026lt;User\u0026gt; wrapper = new LambdaQueryWrapper\u0026lt;\u0026gt;(); wrapper.like(User::getName, \u0026#34;王\u0026#34;); List\u0026lt;User\u0026gt; list = userMapper.selectByMyWhere(wrapper, 20); ${ew.customSqlSegment} 将自动替换为由 Wrapper 生成的 WHERE 条件字符串，且正确处理 where 关键字，非常方便。\n6.3 Service 接口 MP 提供了 IService\u0026lt;T\u0026gt; 和 ServiceImpl\u0026lt;M extends BaseMapper\u0026lt;T\u0026gt;, T\u0026gt;，封装了更高层级的业务逻辑。\n定义 Service 接口：\npublic interface IUserService extends IService\u0026lt;User\u0026gt; { // 自定义方法 } 实现类：\n@Service public class UserServiceImpl extends ServiceImpl\u0026lt;UserMapper, User\u0026gt; implements IUserService { // 实现自定义方法 } 常见操作：\nsave(entity)：新增 saveBatch(list)：批量新增 removeById(id)：根据ID删除 removeBatchByIds(ids)：批量删除（多条 SQL 逐条执行） updateById(entity)：根据ID更新 updateBatchById(list)：批量更新 getById(id)：查询 list()：查询列表 page(page, wrapper)：分页查询 removeByIds 与 removeByBatchIds 的区别？\n其实 removeByIds 在内部默认调用 removeBatchByIds，两者在功能上无本质区别（均通过多 ID 执行 DELETE FROM table WHERE id IN (?,?,...) 一条 SQL）。但在老版本可能前者逐条删除，较新版本已合并为一条 IN 语句。使用上无需纠结，直接用 removeByIds 即可。\nLambda 更新：\nlambdaUpdate() 返回一个 LambdaUpdateChainWrapper，链式操作非常清晰：\nuserService.lambdaUpdate() .eq(User::getId, 1) .set(User::getAge, 28) .update(); // 执行更新 批量新增及效率优化：\nIService 的 saveBatch(list) 默认会一次性提交多条 INSERT 语句，但底层 JDBC 驱动可能仍是一条条发送给数据库。为提高 MySQL 批量插入性能，需要在 JDBC 连接 URL 后添加参数：\njdbc:mysql://localhost:3306/db?rewriteBatchedStatements=true 开启后，驱动会将多条 SQL 合并成一条较长的 SQL 发送，从而显著提升性能。\n这样配置的缺点？\n对于极其大量的数据批量插入，合成的 SQL 长度可能会超过 MySQL 的 max_allowed_packet 限制，导致执行失败。 一旦发生错误，很难定位是哪一条数据出错，不利于调试。 批量操作可能长时间持有表锁，影响并发。 若插入的数据包含自增主键回填需求，批量回填在某些驱动版本下可能不可用或顺序混乱。 因此推荐：控制每批次大小（如 1000 条）并配合 rewriteBatchedStatements 使用。\n6.4 分页插件 MP 分页功能通过插件实现，需先配置 MybatisPlusInterceptor 并添加分页内部拦截器。\n配置类：\n@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } } 使用分页查询：\nPage\u0026lt;User\u0026gt; page = new Page\u0026lt;\u0026gt;(1, 10); // 当前页码，每页条数 Page\u0026lt;User\u0026gt; result = userMapper.selectPage(page, null); // 可传 QueryWrapper List\u0026lt;User\u0026gt; records = result.getRecords(); long total = result.getTotal(); Service 层分页：\nIPage\u0026lt;User\u0026gt; page = userService.page(new Page\u0026lt;\u0026gt;(1, 10), new LambdaQueryWrapper\u0026lt;User\u0026gt;().like(User::getName, \u0026#34;李\u0026#34;)); 分页结果中除了数据，还自动包含了总数、页码等信息，可直接用于前端。\n通用分页实体与 MP 转换：\n实际项目通常会有一个统一响应类，需要把 IPage 转成自定义分页对象，可编写工具方法：\npublic class PageUtils { public static \u0026lt;T\u0026gt; CommonPage\u0026lt;T\u0026gt; convert(IPage\u0026lt;T\u0026gt; page) { CommonPage\u0026lt;T\u0026gt; result = new CommonPage\u0026lt;\u0026gt;(); result.setList(page.getRecords()); result.setTotal(page.getTotal()); result.setCurrent(page.getCurrent()); result.setSize(page.getSize()); return result; } } 7. 扩展功能 7.1 代码生成器 MP 提供强大的代码生成器，可快速生成 Entity、Mapper、Service、Controller 等。推荐使用官方新版。\n引入依赖（可能需要单独添加生成器模块）：\n\u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;com.baomidou\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;mybatis-plus-generator\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;3.5.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; \u0026lt;dependency\u0026gt; \u0026lt;groupId\u0026gt;org.apache.velocity\u0026lt;/groupId\u0026gt; \u0026lt;artifactId\u0026gt;velocity-engine-core\u0026lt;/artifactId\u0026gt; \u0026lt;version\u0026gt;2.3\u0026lt;/version\u0026gt; \u0026lt;/dependency\u0026gt; 示例配置（生成代码）：\npublic class CodeGenerator { public static void main(String[] args) { FastAutoGenerator.create(\u0026#34;jdbc:mysql://localhost:3306/test\u0026#34;, \u0026#34;root\u0026#34;, \u0026#34;password\u0026#34;) .globalConfig(builder -\u0026gt; { builder.author(\u0026#34;baomidou\u0026#34;) // 设置作者 .outputDir(\u0026#34;D://\u0026#34;); // 输出目录 }) .packageConfig(builder -\u0026gt; { builder.parent(\u0026#34;com.example\u0026#34;); // 父包名 }) .strategyConfig(builder -\u0026gt; { builder.addInclude(\u0026#34;user\u0026#34;); // 需要生成的表名 }) .templateEngine(new VelocityTemplateEngine()) .execute(); } } 可根据实际需要调整策略，例如开启 Lombok、设置父类等。\n7.2 静态工具 Db 从 MP 3.5.3 版本开始，提供了静态工具类 Db，可直接进行 CRUD 操作，无需注入 Mapper 或 Service。\n简单使用：\n// 查询列表 List\u0026lt;User\u0026gt; users = Db.lambdaQuery(User.class).like(User::getName, \u0026#34;李\u0026#34;).list(); // 插入 User user = new User(); user.setName(\u0026#34;王五\u0026#34;); Db.save(user); 使用场景：解决循环依赖\n在 Spring 中，如果 Service A 需要依赖 Service B，而 B 又依赖 A，就会产生循环依赖。此时如果一个 Service 在方法中只是临时需要某个表的数据，可通过 Db 静态工具直接操作数据库，从而避免注入另一个 Service，有效解开循环依赖链。\nSpring 如何解决循环依赖？\nSpring 主要通过三级缓存机制解决单例 Bean 的循环依赖：\nsingletonObjects：一级缓存，存放完全初始化好的 Bean。 earlySingletonObjects：二级缓存，存放早期引用（原始对象，未完成属性注入）。 singletonFactories：三级缓存，存放可生成早期引用的 ObjectFactory。\n当 A 创建过程中需要 B，B 创建时又需要 A，Spring 可通过三级缓存获取 A 的早期引用并提前暴露给 B，从而完成创建。但这要求 Bean 是单例且非构造器注入，构造器注入无法解决。而 Db 工具则提供了一种代码层面的解耦方式。 7.3 逻辑删除 逻辑删除即在表中标记数据已删除，而非物理删除，方便数据恢复与审计。\n全局配置（前面已提及）：\nmybatis-plus: global-config: db-config: logic-delete-field: deleted # 实体字段名 logic-delete-value: 1 # 删除后的值 logic-not-delete-value: 0 # 未删除时的值 实体类加注解：\n@Data public class User { @TableLogic private Integer deleted; } 执行 userMapper.deleteById(1L); 实际会生成 UPDATE user SET deleted=1 WHERE id=1 AND deleted=0；查询时自动拼接 deleted=0。\n实际工作推荐做法：\n对于需要保留历史数据的核心业务，推荐使用逻辑删除，并结合数据迁移定时将已逻辑删除的“冷数据”移至归档库，降低业务表体积，维持性能。\n7.4 枚举处理器 将 Java 枚举与数据库字段智能映射，省去手动转换。\n1. 定义枚举并使用 @EnumValue 标记数据库存储值\npublic enum GenderEnum { MALE(1, \u0026#34;男\u0026#34;), FEMALE(2, \u0026#34;女\u0026#34;); @EnumValue // 存入数据库的值 private final int code; private final String desc; // 构造器、getter 略 } 2. 全局配置枚举处理器：\nmybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler 3. 实体类属性使用枚举类型：\npublic class User { private GenderEnum gender; } 插入或查询时，gender 会自动与数据库 int 类型互相转换。\n4. 返回值处理 @JsonValue（前后端交互）： 若需要返回给前端时显示为 JSON 字符串（如 \u0026ldquo;男\u0026rdquo;），可在枚举中标注 @JsonValue：\npublic enum GenderEnum { MALE(1, \u0026#34;男\u0026#34;), FEMALE(2, \u0026#34;女\u0026#34;); @EnumValue private final int code; @JsonValue private final String desc; } 序列化 JSON 时，MALE 会展示为 \u0026quot;男\u0026quot;。\n7.5 JSON 处理器 实体属性可以直接映射 JSON 字段，常用于存储扩展信息。\n实体类：\n@Data @TableName(value = \u0026#34;user\u0026#34;, autoResultMap = true) // 必须开启自动结果映射 public class User { private Long id; @TableField(typeHandler = JacksonTypeHandler.class) private Map\u0026lt;String, Object\u0026gt; extra; // JSON 字段 } 数据库 extra 字段类型为 json 或 varchar。插入时 Map 自动序列化为 JSON 字符串，查询时反序列化回 Map。\n注意：@TableName(autoResultMap = true) 不可省略，否则查询时不会调用 typeHandler 处理该字段。\n7.6 插件功能 MP 的插件体系基于拦截器，可以添加多种功能。\n初始化核心拦截器：\n@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 1. 分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 2. 乐观锁插件 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); // 3. 防止全表更新与删除插件 interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); return interceptor; } } 分页插件：详见 6.4。 乐观锁插件：在实体字段上添加 @Version，更新时自动比对版本号。 防全表更新删除：当执行 update 或 delete 没有 where 条件时，会阻断并抛出异常，有效防止误操作。 ","permalink":"https://lv-blog.pages.dev/posts/programming/backend/mybatisplus/","summary":"\u003ch2 id=\"1-简介\"\u003e1. 简介\u003c/h2\u003e\n\u003cp\u003eMyBatis-Plus（简称 MP）是一个 MyBatis 的增强工具，在 MyBatis 的基础上只做增强不做改变，为简化开发、提高效率而生。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e核心特点：\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e无侵入\u003c/strong\u003e：引入 MP 不会对现有 MyBatis 工程产生影响，犹如丝般顺滑。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e损耗小\u003c/strong\u003e：启动即会自动注入基本 CRUD，性能基本无损耗。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e强大的 CRUD 操作\u003c/strong\u003e：内置通用 Mapper、通用 Service，仅通过少量配置即可实现单表大部分 CRUD 操作。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e支持 Lambda 形式调用\u003c/strong\u003e：通过 Lambda 表达式，安全高效的编写查询条件，防止字段名误写。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e内置代码生成器\u003c/strong\u003e：通过少量配置即可生成 Mapper、Service、Controller 等代码。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e内置分页插件\u003c/strong\u003e：基于 MyBatis 物理分页，开发者无需关心具体操作，配置后即可使用。\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003e官网\u003c/strong\u003e：\u003ca href=\"https://baomidou.com/\"\u003ehttps://baomidou.com/\u003c/a\u003e\u003c/p\u003e\n\u003ch2 id=\"2-基本用法\"\u003e2. 基本用法\u003c/h2\u003e\n\u003ch3 id=\"21-引入依赖\"\u003e2.1 引入依赖\u003c/h3\u003e\n\u003cp\u003e在 Maven 项目 \u003ccode\u003epom.xml\u003c/code\u003e 中添加：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-xml\" data-lang=\"xml\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;groupId\u0026gt;\u003c/span\u003ecom.baomidou\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/groupId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;artifactId\u0026gt;\u003c/span\u003emybatis-plus-boot-starter\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/artifactId\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#f92672\"\u003e\u0026lt;version\u0026gt;\u003c/span\u003e3.5.3.1\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/version\u0026gt;\u003c/span\u003e \u003cspan style=\"color:#75715e\"\u003e\u0026lt;!-- 请按最新版本 --\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#f92672\"\u003e\u0026lt;/dependency\u0026gt;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e如果是传统 Spring 项目，可引入 \u003ccode\u003emybatis-plus\u003c/code\u003e 核心依赖并自行配置。通常 Spring Boot 项目直接使用 starter。\u003c/p\u003e\n\u003ch3 id=\"22-定义实体类\"\u003e2.2 定义实体类\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" style=\"color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;\"\u003e\u003ccode class=\"language-java\" data-lang=\"java\"\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#a6e22e\"\u003e@Data\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e\u003cspan style=\"color:#66d9ef\"\u003epublic\u003c/span\u003e \u003cspan style=\"color:#66d9ef\"\u003eclass\u003c/span\u003e \u003cspan style=\"color:#a6e22e\"\u003eUser\u003c/span\u003e {\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eprivate\u003c/span\u003e Long id;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eprivate\u003c/span\u003e String name;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eprivate\u003c/span\u003e Integer age;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e    \u003cspan style=\"color:#66d9ef\"\u003eprivate\u003c/span\u003e String email;\n\u003c/span\u003e\u003c/span\u003e\u003cspan style=\"display:flex;\"\u003e\u003cspan\u003e}\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003ch3 id=\"23-编写-mapper-接口\"\u003e2.3 编写 Mapper 接口\u003c/h3\u003e\n\u003cp\u003e继承 \u003ccode\u003eBaseMapper\u0026lt;T\u0026gt;\u003c/code\u003e 即可获得 CRUD 能力：\u003c/p\u003e","title":"MyBatis-Plus 学习"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/musicsheet/enrique-granados-apparitions-romantic-waltzes-transcription/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/e7f25431220e34615bea0a00_video_0.mp4\" controls\u003e\u003c/video\u003e","title":"恩里克·格拉纳多斯 - 幽靈浪漫圓舞曲 听译谱"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/musicsheet/original/piano-thief/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/e7f2543102be25610c130800_video_0.mp4\" controls\u003e\u003c/video\u003e","title":"原创作品 钢琴小偷"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/musicsheet/a-letter-for-mama/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/%E0%B9%80%E0%B8%A3%E0%B8%B5%E0%B8%A2%E0%B8%87%E0%B8%84%E0%B8%A7%E0%B8%B2%E0%B8%A1%E0%B9%80%E0%B8%A3%E0%B8%B7%E0%B9%88%E0%B8%AD%E0%B8%87%E0%B9%81%E0%B8%A1%E0%B9%88%E0%B8%84%E0%B8%B4%E0%B8%94%E0%B8%96%E0%B8%B6%E0%B8%87%E0%B9%81%E0%B8%A1%E0%B9%88%20_%20%E0%B9%80%E0%B8%A3%E0%B8%B5%E0%B8%A2%E0%B8%87%E0%B8%84%E0%B8%A7%E0%B8%B2%E0%B8%A1%E0%B9%80%E0%B8%A3%E0%B8%B7%E0%B9%88%E0%B8%AD%E0%B8%87%E0%B9%81%E0%B8%A1%E0%B9%88%E5%86%99%E7%BB%99_video_0.mp4\" controls\u003e\u003c/video\u003e","title":"写给妈妈的信(让她开心) 改编谱"},{"content":"当世界陷入毁灭，人类文明崩塌。\n没有政府，没有法律，没有网络，没有秩序。\n你和另外99名幸存者被困在一片面积数百平方公里的废土之中。\n这里有食物、枪支、车辆、药品、避难所。\n这里也有数以百万计不断进化的丧尸。\n而你的目标只有一个：\n活过100天。\n第一章：末日降临 游戏开始时，100名玩家随机出生在超大型开放地图的不同区域。\n地图包含：\n废弃城市 郊区住宅 医院 军事基地 商场 仓库 工业园区 森林 雪山 沙漠 港口 机场 玩家初始只有：\n一套普通衣服 一部手机 少量食物 简易工具 其余一切都需要自行探索。\n生存法则 玩家必须面对：\n饥饿 感染 寒冷 高温 疲劳 丧尸袭击 其他玩家 每一天都可能是最后一天。\n玩家关系 游戏没有阵营。\n没有正义与邪恶。\n你可以选择任何生存方式。\n合作派 通过无线电寻找其他幸存者。\n建立据点。\n共享资源。\n组织巡逻。\n共同抵御尸潮。\n独狼派 独自行动。\n隐藏踪迹。\n避免一切接触。\n依靠个人能力活到最后。\n掠夺者 猎杀其他玩家。\n抢夺资源。\n控制空投。\n袭击幸存者据点。\n玩家死亡后：\n其全部物资掉落。\n这意味着一次成功伏击可能获得数十天的生存资源。\n胜利条件 游戏存在三种结局。\n人类胜利 坚持生存100天。\n第100天军方发动最后一次救援行动。\n武装直升机抵达地图。\n登机成功的人类获得胜利。\n丧尸胜利 被感染后完成变异。\n最终感染全部幸存人类。\n世界彻底沦陷。\n黑暗胜利 你并非丧尸。\n但通过掠夺、封锁资源、控制空投等方式。\n让所有人类无法活过100天。\n最终人类灭绝。\n你获得特殊胜利。\n死亡条件 玩家可能因为以下原因出局：\n被玩家击杀 被丧尸击杀 饥饿死亡 极端环境死亡 变异后被玩家击杀 所有人类全部死亡 死亡后无法复活。\n感染系统 这是游戏最核心的机制。\n当玩家受到严重抓伤、撕咬或开放性伤口攻击时：\n有概率感染病毒。\n感染第一天 身体开始异常。\n表现：\n画面颜色逐渐灰暗 呼吸声加重 偶尔出现幻听 玩家仍可正常行动。\n感染第二天 病毒开始扩散。\n表现：\n移动速度下降 耐力恢复减慢 若未使用药物控制 每隔一段时间：\n自动失去2点生命值。\n生命值最低降至10点。\n不会继续下降。\n感染第三天 病毒进入神经系统。\n表现：\n无法站立 只能爬行 无法使用枪械 无法驾驶车辆 此时队友面临选择：\n结束他的生命 避免尸变。\n寻找疫苗 如果能在24小时内注射疫苗：\n玩家恢复正常。\n感染第四天 彻底变异。\n生命值：\n1000点\n玩家正式成为丧尸。\n无法逆转。\n永远失去人类身份。\n丧尸玩家 变异后的体验与人类完全不同。\n丧尸能力 拥有：\n夜视能力 无需进食 无限耐力 不受温度影响 但无法使用：\n枪械 车辆 人类设备 丧尸感知系统 在丧尸视角中：\n光线极度醒目 黑夜中的手电筒。\n篝火。\n汽车大灯。\n甚至远处建筑中的灯光。\n都会像信号弹一样显眼。\n声音感知增强 脚步声。\n枪声。\n说话声。\n无线电广播。\n都可能暴露幸存者位置。\n声音系统 末日最大的敌人并不是丧尸。\n而是声音。\n枪声 会吸引附近尸群。\n开麦说话 如果周围存在丧尸。\n声音可能直接暴露位置。\n发电机 持续制造噪音。\n吸引尸群。\n车辆 引擎声会吸引道路上的丧尸。\n速度越快。\n吸引范围越大。\n车辆系统 玩家可以驾驶：\n轿车 SUV 面包车 卡车 摩托车 撞击系统 车辆可以碾压丧尸。\n但普通车辆十分脆弱。\n连续撞击：\n保险杠损坏 发动机受损 轮胎爆裂 最终报废。\n玩家需要收集：\n钢板 防撞架 焊接材料 改造末日战车。\n疫苗争夺战 军方并未完全放弃幸存者。\n每隔几天：\n随机空投补给。\n内容包括：\n武器 药品 食物 弹药 每隔10天：\n出现特殊空投。\n其中必定包含：\n疫苗。\n当感染人数增加。\n疫苗便成为最昂贵的资源。\n甚至可能引发数十名玩家的大规模战争。\n装备与服装系统 衣服不仅是外观。\n更是护甲。\n重型服装 例如：\n羽绒服 摩托车服 工装服 特点：\n高防御 降低感染概率 缺点：\n移动速度下降 耐力消耗增加 炎热地区甚至可能因为中暑而被迫脱掉。\n轻型服装 例如：\nT恤 衬衫 运动装 特点：\n移动灵活 缺点：\n几乎没有防御 容易被感染。\n衣物耐久 受到攻击后：\n服装会逐渐破损。\n护甲下降。\n玩家必须持续寻找新的装备。\n手机系统 每位玩家出生时拥有一部手机。\n末日后：\n通讯基站全部瘫痪。\n手机无法拨打电话。\n没有GPS。\n没有互联网。\n但仍然具有重要功能：\n热点组网 两部手机靠近后：\n可以建立局域网连接。\n实现：\n地图共享 照片传输 文字信息 距离过远：\n手机自动报警。\n拍照取证 记录：\n玩家犯罪 资源位置 尸群规模 甚至可以作为幸存者组织中的证据。\n手电筒 照亮黑暗。\n同时也暴露自己。\n温度检测 判断环境是否适宜生存。\n电量系统 手机需要充电。\n可通过：\n发电机 太阳能板 电池更换 维持使用。\n高级手机拥有：\n更长续航 更远热点距离 更强夜视摄像头 城市设施 地图中的建筑都有独特作用。\n医院 获得：\n药品 绷带 肾上腺素 疫苗线索 军火店 获得：\n枪械 弹药 防弹衣 服装店 获得：\n更高护甲服装 加油站 获得：\n汽油 柴油 汽车4S店 获得：\n完整车辆 改装零件 超市 获得：\n食物 饮料 饮料同样提供饱食度。\n理发店 随着时间推移：\n角色头发不断增长。\n理发店可以恢复形象。\n美容院 玩家可进行角色改造。\n打造末日中的个人风格。\n耐力系统 所有行动都会消耗耐力。\n包括：\n奔跑 攀爬 近战 搬运物资 耐力耗尽后：\n角色行动大幅减慢。\n医院中获得的：\n肾上腺素\n可以短时间：\n无限冲刺 无视疲劳 但药效结束后会陷入虚弱状态。\n尸潮进化 最可怕的不是玩家。\n而是时间。\n第1天的丧尸：\n缓慢。\n愚蠢。\n容易击杀。\n第50天：\n尸群开始出现变种。\n奔跑者 巨力者 尖叫者 第100天：\n世界进入地狱。\n数万只高级变种组成的超级尸潮将向所有幸存者据点发起总攻。\n如果玩家们此前只顾互相厮杀。\n那么他们最终会发现：\n真正的敌人从来不是彼此。\n而是那个正在不断进化的末日。\n核心魅力 这不是一个单纯的吃鸡游戏。\n也不是一个普通的丧尸游戏。\n它更像是一场持续100天的社会实验。\n你可以成为英雄。\n成为独狼。\n成为军阀。\n成为掠夺者。\n甚至成为毁灭人类文明的最后一只丧尸。\n而在第100天直升机出现的那一刻，所有活下来的人都会明白：他们战胜的从来不只是丧尸，而是人性、恐惧与绝望本身。\n","permalink":"https://lv-blog.pages.dev/posts/essay/zombie-game/","summary":"\u003cp\u003e当世界陷入毁灭，人类文明崩塌。\u003c/p\u003e\n\u003cp\u003e没有政府，没有法律，没有网络，没有秩序。\u003c/p\u003e\n\u003cp\u003e你和另外99名幸存者被困在一片面积数百平方公里的废土之中。\u003c/p\u003e\n\u003cp\u003e这里有食物、枪支、车辆、药品、避难所。\u003c/p\u003e\n\u003cp\u003e这里也有数以百万计不断进化的丧尸。\u003c/p\u003e\n\u003cp\u003e而你的目标只有一个：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e活过100天。\u003c/strong\u003e\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"第一章末日降临\"\u003e第一章：末日降临\u003c/h2\u003e\n\u003cp\u003e游戏开始时，100名玩家随机出生在超大型开放地图的不同区域。\u003c/p\u003e\n\u003cp\u003e地图包含：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e废弃城市\u003c/li\u003e\n\u003cli\u003e郊区住宅\u003c/li\u003e\n\u003cli\u003e医院\u003c/li\u003e\n\u003cli\u003e军事基地\u003c/li\u003e\n\u003cli\u003e商场\u003c/li\u003e\n\u003cli\u003e仓库\u003c/li\u003e\n\u003cli\u003e工业园区\u003c/li\u003e\n\u003cli\u003e森林\u003c/li\u003e\n\u003cli\u003e雪山\u003c/li\u003e\n\u003cli\u003e沙漠\u003c/li\u003e\n\u003cli\u003e港口\u003c/li\u003e\n\u003cli\u003e机场\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e玩家初始只有：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e一套普通衣服\u003c/li\u003e\n\u003cli\u003e一部手机\u003c/li\u003e\n\u003cli\u003e少量食物\u003c/li\u003e\n\u003cli\u003e简易工具\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e其余一切都需要自行探索。\u003c/p\u003e\n\u003chr\u003e\n\u003ch1 id=\"生存法则\"\u003e生存法则\u003c/h1\u003e\n\u003cp\u003e玩家必须面对：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e饥饿\u003c/li\u003e\n\u003cli\u003e感染\u003c/li\u003e\n\u003cli\u003e寒冷\u003c/li\u003e\n\u003cli\u003e高温\u003c/li\u003e\n\u003cli\u003e疲劳\u003c/li\u003e\n\u003cli\u003e丧尸袭击\u003c/li\u003e\n\u003cli\u003e其他玩家\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e每一天都可能是最后一天。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"玩家关系\"\u003e玩家关系\u003c/h2\u003e\n\u003cp\u003e游戏没有阵营。\u003c/p\u003e\n\u003cp\u003e没有正义与邪恶。\u003c/p\u003e\n\u003cp\u003e你可以选择任何生存方式。\u003c/p\u003e\n\u003ch3 id=\"合作派\"\u003e合作派\u003c/h3\u003e\n\u003cp\u003e通过无线电寻找其他幸存者。\u003c/p\u003e\n\u003cp\u003e建立据点。\u003c/p\u003e\n\u003cp\u003e共享资源。\u003c/p\u003e\n\u003cp\u003e组织巡逻。\u003c/p\u003e\n\u003cp\u003e共同抵御尸潮。\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"独狼派\"\u003e独狼派\u003c/h3\u003e\n\u003cp\u003e独自行动。\u003c/p\u003e\n\u003cp\u003e隐藏踪迹。\u003c/p\u003e\n\u003cp\u003e避免一切接触。\u003c/p\u003e\n\u003cp\u003e依靠个人能力活到最后。\u003c/p\u003e\n\u003chr\u003e\n\u003ch3 id=\"掠夺者\"\u003e掠夺者\u003c/h3\u003e\n\u003cp\u003e猎杀其他玩家。\u003c/p\u003e\n\u003cp\u003e抢夺资源。\u003c/p\u003e\n\u003cp\u003e控制空投。\u003c/p\u003e\n\u003cp\u003e袭击幸存者据点。\u003c/p\u003e\n\u003cp\u003e玩家死亡后：\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e其全部物资掉落。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e这意味着一次成功伏击可能获得数十天的生存资源。\u003c/p\u003e\n\u003chr\u003e\n\u003ch1 id=\"胜利条件\"\u003e胜利条件\u003c/h1\u003e\n\u003cp\u003e游戏存在三种结局。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"人类胜利\"\u003e人类胜利\u003c/h2\u003e\n\u003cp\u003e坚持生存100天。\u003c/p\u003e\n\u003cp\u003e第100天军方发动最后一次救援行动。\u003c/p\u003e\n\u003cp\u003e武装直升机抵达地图。\u003c/p\u003e\n\u003cp\u003e登机成功的人类获得胜利。\u003c/p\u003e\n\u003chr\u003e\n\u003ch2 id=\"丧尸胜利\"\u003e丧尸胜利\u003c/h2\u003e\n\u003cp\u003e被感染后完成变异。\u003c/p\u003e","title":"一个僵尸生存游戏的想法"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/musicsheet/konosougennohikariwo_himekami_shinrabansho1998_pianoarrangement/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/KonoSougenNoHikariWo_Himekami_Shinrabansho1998_PianoArrangement.mp4\" controls\u003e\u003c/video\u003e","title":"この草原の光を - 姬神 - 森羅万象（1998）改编谱"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/musicsheet/original/blown_away_sheet_music_piano_by_locrianfifth/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/Blown_Away_Sheet_Music_Piano_by_LocrianFifth_video_0.mp4\" controls\u003e\u003c/video\u003e","title":"原创钢琴曲 被吹翻的乐谱"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/musicsheet/original/lang-ge-lang-er-lang/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/%5Be402249.gif%5D%5Be401136.gif%5D%5Be400234.gif%5D%5Be401136.gi_video_0.mp4\" controls\u003e\u003c/video\u003e","title":"原创钢琴小片段 郎个郎二郎"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/musicsheet/original/when_blue_wind_came_let_me_be_wind/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/When_Blue_Wind_Came_Let_Me_Be_Wind.mp4\" controls\u003e\u003c/video\u003e","title":"原创歌曲 当她忧郁的时候，来了一阵风，突然她说让我化成风"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/musicsheet/original/forget_demo_piano_by_locrianfifth_feat_shiyi/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/Forget_Demo_Piano_by_LocrianFifth_feat_Shiyi_video_0.mp4\" controls\u003e\u003c/video\u003e","title":"原创歌曲 忘记"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/performance/op39_summer_night_waltzes_no3_kiss_masonma/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/Op.39%20Summer%20Night%20Waltzes%20No.3%20%E2%80%9CKiss%E2%80%9D%20-%20Mason%20Ma_video_0.mp4\" controls\u003e\u003c/video\u003e","title":"钢琴演奏 Op.39 Summer Night Waltzes No.3 “Kiss” - Mason Ma"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/performance/song-from-a-secret-garden/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/e7f25431f3117a60705d0900_video_0.mp4\" controls\u003e\u003c/video\u003e","title":"钢琴演奏 Song from a Secret Garden - Rolf Løvland"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/performance/energy-flow/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/e7f2543149b67660e5170000_video_0.mp4\" controls\u003e\u003c/video\u003e","title":"钢琴演奏 Energy Flow - 坂本龙一"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/musicsheet/original/maybe-a-little-rock/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/e7f254316a1c6c606c490900_video_0.mp4\" controls\u003e\u003c/video\u003e","title":"原创音乐 或许有一点摇滚？"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/performance/barcarolle/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/e7f25431c6de4860ef730e00_video_0.mp4\" controls\u003e\u003c/video\u003e","title":"钢琴演奏 船歌 - 门徳尔松"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/performance/zaocao/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/zaocao.mp4\" controls\u003e\u003c/video\u003e","title":"钢琴演奏 早操 - 周杰伦"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/musicsheet/bluedragon/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/bluedragon.mp4\" controls\u003e\u003c/video\u003e","title":"Blue Dragon - 泽野弘之 改编谱"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/musicsheet/apparitions_romantic_waltzes_pastoral_granados_revised_locrianfifth/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/e7f25431172f12603cc40700_video_0.mp4\" controls\u003e\u003c/video\u003e","title":"幽灵·浪漫圆舞曲·田园 听译谱"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/musicsheet/apparitions_romantic_waltzes_sensitive_granados_revised_locrianfifth/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/e7f25431173a6b5ff6750b00_video_0.mp4\" controls\u003e\u003c/video\u003e","title":"Apparitions Romantic Waltzes Sensitive – Enrique Granados 听译谱"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/performance/dream_rabpit/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/dream_rabpit.mp4\" controls\u003e\u003c/video\u003e","title":"钢琴演奏 Dream - Rabpit"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/musicsheet/original/merry_christmas_mr_lawrence/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/Merry_Christmas_Mr_Lawrence.mp4\" controls\u003e\u003c/video\u003e","title":"钢琴演奏 圣诞快乐劳伦斯先生 - 坂本龙一"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/musicsheet/luxiaoyu/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/e7f25431172f12603cc40700_video_0.mp4\" controls\u003e\u003c/video\u003e","title":"钢琴演奏 路小雨 - 周杰伦"},{"content":" ","permalink":"https://lv-blog.pages.dev/posts/piano/performance/the-last-steam-engine-train/","summary":"\u003cvideo src=\"https://img.wathan.cn/qzone_videos/the-last-steam-engine-train.mp4\" controls\u003e\u003c/video\u003e","title":"吉他演奏 The Last Steam Engine Train - John Fahey"}]