<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Fuwari</title><description>Demo Site</description><link>https://jinliye.github.io/</link><language>en</language><item><title>MoMo</title><link>https://jinliye.github.io/Blog/posts/techinterview/momo/</link><guid isPermaLink="true">https://jinliye.github.io/Blog/posts/techinterview/momo/</guid><pubDate>Fri, 29 May 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;笔试&lt;/h2&gt;
&lt;h3&gt;数据库&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;表 t 结构为 (id INT PRIMARY KEY, age INT)。现有记录(1, 10), (5, 20), (10, 30)&lt;/p&gt;
&lt;p&gt;分析下面的事务执行，在 RR 隔离级别下，事务 A 会采取哪种方案？&lt;/p&gt;
&lt;p&gt;事务 A 执行：UPDATE t SET age = 21 WHERE id = 5;&lt;/p&gt;
&lt;p&gt;索引轴加锁方案：
方案 1（普通索引）：锁定 (1, 5] 和 (5, 10)
方案 2（唯一索引/主键）：只锁定 id = 5 这一行&lt;/p&gt;
&lt;p&gt;A 方案 1&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;B 方案 2&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;C 先执行方案 1，发现是主键后再降级为方案 2&lt;/p&gt;
&lt;p&gt;D 同时执行两个方案&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;相关知识：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;隔离级别：Repeatable Read（RR），通过Gap Lock+Next-Key Lock 解决幻读（在同一个事务内，多次执行相同的查询语句，得到的结果集行数不一样）问题。&lt;/li&gt;
&lt;li&gt;三种锁：
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Record Lock（行锁）：锁住索引记录。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Gap Lock（间隙锁）：锁住索引记录之间的间隙，不锁数据本身。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Next-Key Lock（临键锁）：锁住索引记录和索引记录之间的间隙，锁范围：左开右闭区间 (a, b]。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;exp:
数据：1，5，10
临键锁区间：
(-∞,1]、(1,5]、(5,10]、(10,+∞)&lt;/p&gt;
&lt;p&gt;如果命中 id=5：
锁住 (1,5]，锁住 5 这行（记录锁）
锁住 1~5 之间的空隙（间隙锁）→ 别人不能修改 5，也不能插入 2、3、4&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Gap Lock 锁间隙，不让插入；Next-Key Lock = 锁行 + 锁间隙（默认）；&lt;strong&gt;唯一索引 / 主键 等值命中 → 降级为单行记录锁&lt;/strong&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 EXPLAIN 的 Extra 字段中，出现 Using index condition 代表：&lt;/p&gt;
&lt;p&gt;A 使用了覆盖索引&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;B 使用了索引下推（Index Condition Pushdown）优化&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;C 进行了全表扫描&lt;/p&gt;
&lt;p&gt;D 索引失效&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;相关知识：
&lt;img src=&quot;image-1.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;三个事务按顺序执行如下操作（表 t 有唯一索引 a，目前为空）：&lt;/p&gt;
&lt;p&gt;T1：INSERT INTO t(id, a) VALUES(1, 10); （正常执行）&lt;/p&gt;
&lt;p&gt;T2：INSERT INTO t(id, a) VALUES(2, 10); （阻塞，等待 S 锁）&lt;/p&gt;
&lt;p&gt;T3：INSERT INTO t(id, a) VALUES(3, 10); （阻塞，等待 S 锁）&lt;/p&gt;
&lt;p&gt;T1：ROLLBACK; （T1 回滚）&lt;/p&gt;
&lt;p&gt;结果：此时 T2 和 T3 之间会发生什么？&lt;/p&gt;
&lt;p&gt;A T2 执行成功，T3 继续等待&lt;/p&gt;
&lt;p&gt;B T2 和 T3 都会成功&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;C 产生死锁（Deadlock），MySQL 会回滚其中一个&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;D T2 和 T3 都会回滚&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;下图展示了 MySQL 在更新一条记录时，Redo Log 和 Binlog 的写入逻辑。这是保证数据库崩溃恢复后“主从一致”的核心机制：&lt;/p&gt;
&lt;p&gt;Step 1：写入 Redo Log（prepare 状态）&lt;/p&gt;
&lt;p&gt;Step 2：写入 Binlog（写入磁盘）&lt;/p&gt;
&lt;p&gt;Step 3：写入 Redo Log（commit 状态）&lt;/p&gt;
&lt;p&gt;如果在 Step 2 执行完、Step 3 尚未执行时，数据库突然断电宕机，重启后该事务会如何处理？&lt;/p&gt;
&lt;p&gt;A 直接丢弃，因为事务没有完全提交&lt;/p&gt;
&lt;p&gt;B 依然提交，因为 Binlog 已经完整写入，可以保证主从数据一致&lt;/p&gt;
&lt;p&gt;C 报错并要求人工干预&lt;/p&gt;
&lt;p&gt;D 回滚，因为 Redo Log 还处于 prepare 状态&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;相关知识：
题目的核心机制是MySQL的&lt;strong&gt;两阶段提交（2PC）&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-3.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;MySQL 崩溃重启后，会对处于 prepare 状态的 Redo Log 事务进行检查：&lt;/p&gt;
&lt;p&gt;判断规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;如果 Binlog 完整写入（事务的 Binlog 存在且完整） → 提交该事务&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果 Binlog 不完整或不存在 → 回滚该事务&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么这样设计？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Binlog 完整：说明主库已经记录了该事务的变更。如果回滚，主库没有这条数据，但从库通过 Binlog 同步后却有这条数据，导致主从不一致。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Binlog 不完整：说明事务尚未同步到 Binlog，主库回滚后，从库也不会看到，保持一致。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在 Repeatable Read（RR）级别下，事务 A 和事务 B 按如下时序执行（假设表 t 中初始没有 id=5 的数据）：&lt;/p&gt;
&lt;p&gt;时间	事务 A（Transaction A）	事务 B（Transaction B）&lt;/p&gt;
&lt;p&gt;T1	BEGIN；	BEGIN；&lt;/p&gt;
&lt;p&gt;T2	SELECT * FROM t WHERE id=5；（空）	INSERT INTO t(id) VALUES(5)；&lt;/p&gt;
&lt;p&gt;T3		COMMIT；&lt;/p&gt;
&lt;p&gt;T4	UPDATE t SET name=&apos;X&apos; WHERE id=5；&lt;/p&gt;
&lt;p&gt;T5	SELECT * FROM t WHERE id=5；（???）&lt;/p&gt;
&lt;p&gt;请问在 T5 时刻，事务 A 查询 id=5 的结果是：&lt;/p&gt;
&lt;p&gt;A 依然为空（符合可重复读）&lt;/p&gt;
&lt;p&gt;B 能够查到 id=5 的记录，且 name 为 &apos;X&apos;&lt;/p&gt;
&lt;p&gt;C 事务 A 会在 T5 报错，提示“数据已存在”&lt;/p&gt;
&lt;p&gt;D 事务 A 会在 T4 被阻塞，直到超时&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;相关知识：
在 RR 隔离级别下，快照读（普通 SELECT）与当前读（UPDATE / SELECT ... FOR UPDATE）使用不同的数据版本。事务的普通 SELECT 默认读取事务开始时的快照，但一旦事务内执行了当前读操作（如 UPDATE），就会读取最新的已提交数据，并且该事务后续的普通 SELECT 也会基于更新后的版本，从而“突破”可重复读的旧快照限制。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;读已提交（Read Committed）隔离级别下，Read View 的生成时机是：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A 事务启动时生成一次，后续复用&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;B 每一条 SELECT 语句执行时都重新生成&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;C 只有执行 CUD 操作时才生成&lt;/p&gt;
&lt;p&gt;D 整个数据库生命周期只生成一次&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;image-5.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;计算机基础&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Java 反射机制不能实现的功能是（）&lt;/p&gt;
&lt;p&gt;A 在运行时判断对象所属的类&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;B 在运行时修改常量的值&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;C 在运行时构造类的对象&lt;/p&gt;
&lt;p&gt;D 在运行时调用私有方法&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;相关知识：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A：通过 Object.getClass() 方法可以获取对象的运行时类，obj.getClass().getName() 可以得到类的全限定名。&lt;/li&gt;
&lt;li&gt;B: 常量通常用 static final 修饰，Java 编译时会进行常量折叠（Constant Folding），将常量的值直接内联到字节码中。&lt;/li&gt;
&lt;li&gt;C: 通过 Class.newInstance() 方法可以创建类的对象。&lt;/li&gt;
&lt;li&gt;D: 通过反射可以调用私有方法，通过 Method.setAccessible(true) 可以绕过 Java 的访问控制检查。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;在 TCP 三次握手过程中，服务端收到客户端发送的 SYN 包并回复 SYN+ACK 后，服务端进入什么状态？&lt;/p&gt;
&lt;p&gt;A ESTABLISHED&lt;/p&gt;
&lt;p&gt;B SYN_SENT&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;C SYN_RCVD&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;D LISTEN&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;相关知识：
&lt;img src=&quot;image.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;HashMap在JDK 1.8中，当链表长度超过8时，会将链表转换为红黑树。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如何使用两个栈来实现浏览器的前进和后退功能？&lt;/p&gt;
&lt;p&gt;栈 A（后退栈）：记录用户访问过的页面历史。
访问新页面时，当前页面压入栈 A。
点击 “后退” 时，从栈 A 弹出页面，压入栈 B。&lt;/p&gt;
&lt;p&gt;栈 B（前进栈）：记录用户后退过的页面。
点击 “前进” 时，从栈 B 弹出页面，压入栈 A。
访问新页面时，栈 B 清空，保证前进路径唯一。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;JVM的内存区域中，线程私有的部分是&lt;/p&gt;
&lt;p&gt;A. 堆  B. 方法区  C. 虚拟机栈  D. 直接内存&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;相关知识：
&lt;img src=&quot;image-2.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;关于 StringBuilder 和 StringBuffer 的区别，说法正确的是：&lt;/p&gt;
&lt;p&gt;A StringBuilder 是线程安全的，效率较低。&lt;/p&gt;
&lt;p&gt;B StringBuffer 是线程安全的，效率较低。&lt;/p&gt;
&lt;p&gt;C 两者都是不可变的。&lt;/p&gt;
&lt;p&gt;D 在单线程循环内拼接大量字符串，推荐使用 StringBuilder。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;相关知识：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;StringBuilder 和 StringBuffer 都是用于拼接字符串的类，但 StringBuffer 是线程安全的，而 StringBuilder 不是。&lt;/li&gt;
&lt;li&gt;StringBuilder 和 StringBuffer 都是可变的，它们的方法不会创建新的对象，而是直接修改原对象。&lt;/li&gt;
&lt;li&gt;在单线程循环内拼接大量字符串时，推荐使用 StringBuilder，因为它效率更高。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;HashMap 在扩容（Resize）时，下列描述正确的是：&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A 扩容后，每个元素在数组中的索引位置一定保持不变。&lt;/p&gt;
&lt;p&gt;B 扩容后的容量是原来的 1.5 倍。&lt;/p&gt;
&lt;p&gt;C 扩容过程涉及所有 key 的重新 Hash 计算。&lt;/p&gt;
&lt;p&gt;D 扩容是为了减少 Hash 冲突带来的性能下降。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;相关知识：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HashMap 在扩容时，会将数组长度扩大为原来的两倍，并且重新计算每个元素在数组中的索引位置。因此，扩容后，每个元素在数组中的索引位置不一定保持不变。&lt;/li&gt;
&lt;li&gt;HashMap 的扩容容量是原来的两倍，而不是 1.5 倍，ArrayList是1.5倍。&lt;/li&gt;
&lt;li&gt;扩容过程不需要重新计算 hash，只判断新增 bit。&lt;/li&gt;
&lt;li&gt;扩容是为了解决 Hash 冲突带来的性能下降。
&lt;img src=&quot;image-4.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Java中&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;其他&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;AI提示词的5C原则：
&lt;ul&gt;
&lt;li&gt;Character&lt;/li&gt;
&lt;li&gt;Cause&lt;/li&gt;
&lt;li&gt;Constraint&lt;/li&gt;
&lt;li&gt;Contingency&lt;/li&gt;
&lt;li&gt;Calibration&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;example
&lt;ul&gt;
&lt;li&gt;“帮我重构这段300行的订单处理代码。”&lt;/li&gt;
&lt;li&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Character（角色）
你是一位拥有10年经验的Java架构师，专注于领域驱动设计（DDD）和可测试性。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Cause（原因）
当前订单处理代码有以下问题（请基于这些原因进行重构）：&lt;/p&gt;
&lt;p&gt;一个方法内包含：参数校验、数据库查询、折扣计算、库存扣减、邮件发送、日志记录。&lt;/p&gt;
&lt;p&gt;单元测试几乎无法写（mock依赖过多）。&lt;/p&gt;
&lt;p&gt;新增一种支付方式需要修改核心类（违反开闭原则）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Constraint（约束）
不能改数据库表结构。&lt;/p&gt;
&lt;p&gt;不能引入新的消息队列或外部中间件。&lt;/p&gt;
&lt;p&gt;必须保持对外接口的签名不变。&lt;/p&gt;
&lt;p&gt;重构后代码行数不超过350行（可适度增加但不膨胀）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Contingency（条件 / 边界）
如果某一步重构会导致性能下降超过10%（对比原代码的简单压测），请标明并提出替代方案。&lt;/p&gt;
&lt;p&gt;如果某些逻辑必须保持顺序执行（如先扣库存后扣款），请注明不能并发的部分。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Calibration（校准 / 验证）
重构后请提供：&lt;/p&gt;
&lt;p&gt;新代码的目录结构或类职责说明（几句话即可）。&lt;/p&gt;
&lt;p&gt;一个单元测试示例（仅针对折扣计算逻辑）。&lt;/p&gt;
&lt;p&gt;指出哪里体现了单一职责原则。&lt;/p&gt;
&lt;p&gt;对比原代码：可维护性提升的具体表现（3个要点）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;算法题&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/rotting-oranges/description/?envType=study-plan-v2&amp;amp;envId=top-100-liked&quot;&gt;腐烂橘子&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/course-schedule-iii/description/&quot;&gt;课程表&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>Go语言相关知识</title><link>https://jinliye.github.io/Blog/posts/go/pms-project/</link><guid isPermaLink="true">https://jinliye.github.io/Blog/posts/go/pms-project/</guid><pubDate>Thu, 09 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Go的垃圾回收机制&lt;/h2&gt;
&lt;h3&gt;一、这部分笔记的大纲&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;为什么程序需要垃圾回收（GC）&lt;/li&gt;
&lt;li&gt;Go 为什么选择自动内存管理&lt;/li&gt;
&lt;li&gt;Go 的内存分区基础：栈、堆、全局区&lt;/li&gt;
&lt;li&gt;栈上分配和堆上分配：逃逸分析的作用&lt;/li&gt;
&lt;li&gt;Go GC 的核心思路：可达性分析 + 标记清扫&lt;/li&gt;
&lt;li&gt;三色标记法到底在解决什么问题&lt;/li&gt;
&lt;li&gt;并发 GC、STW（Stop The World）与写屏障&lt;/li&gt;
&lt;li&gt;Go 为什么不采用传统分代 GC&lt;/li&gt;
&lt;li&gt;Go GC 的整体执行流程&lt;/li&gt;
&lt;li&gt;结合伪代码理解一次完整回收&lt;/li&gt;
&lt;li&gt;Go 这套设计的优点、代价与适用场景&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;二、为什么需要垃圾回收&lt;/h3&gt;
&lt;p&gt;程序运行时会不断申请内存。如果一块内存已经没有任何变量再使用它，但程序又没有把它释放掉，这块内存就会一直占着空间，这就是“垃圾”。&lt;/p&gt;
&lt;p&gt;如果没有垃圾回收，主要会出现两个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内存泄漏：不用的对象还留在内存里，越积越多&lt;/li&gt;
&lt;li&gt;管理复杂：程序员需要手动 &lt;code&gt;malloc/free&lt;/code&gt; 或 &lt;code&gt;new/delete&lt;/code&gt;，很容易释放早了、释放晚了，甚至重复释放&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从语言设计角度看，内存管理本质上是在平衡三件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;性能&lt;/li&gt;
&lt;li&gt;易用性&lt;/li&gt;
&lt;li&gt;安全性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C/C++ 更偏向性能和控制权，所以交给程序员自己管理。Go 更强调工程效率、并发编程体验和服务端稳定性，因此选择自动垃圾回收。&lt;/p&gt;
&lt;h3&gt;三、Go 为什么选择自动内存管理&lt;/h3&gt;
&lt;p&gt;Go 主要面向的是服务器程序、网络服务、中间件、云原生基础设施。这类程序的特点是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生命周期长，不是“运行一下就退出”&lt;/li&gt;
&lt;li&gt;并发多，协程数量大&lt;/li&gt;
&lt;li&gt;对延迟敏感，但也要求开发效率&lt;/li&gt;
&lt;li&gt;代码维护者很多，出错成本高&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果仍然使用手动内存管理，会出现两个明显问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;开发心智负担过高&lt;br /&gt;
并发场景下，一个对象可能被多个 goroutine 间接引用，谁来释放、什么时候释放，判断非常麻烦。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;稳定性风险高&lt;br /&gt;
手动释放容易导致悬挂指针、野指针、双重释放等问题，而这些问题在服务端通常很难排查。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;所以 Go 的设计选择不是“追求绝对最快”，而是“在足够高性能的前提下，把内存安全和开发效率做成默认能力”。&lt;/p&gt;
&lt;h3&gt;四、Go 的内存分区：栈、堆、全局区&lt;/h3&gt;
&lt;p&gt;理解 GC 前，先要知道对象可能放在哪里。&lt;/p&gt;
&lt;h4&gt;1. 栈（stack）&lt;/h4&gt;
&lt;p&gt;栈一般存放：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数参数&lt;/li&gt;
&lt;li&gt;局部变量&lt;/li&gt;
&lt;li&gt;返回地址&lt;/li&gt;
&lt;li&gt;调用帧相关信息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;栈的特点是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分配和释放极快&lt;/li&gt;
&lt;li&gt;生命周期跟函数调用强绑定&lt;/li&gt;
&lt;li&gt;不需要 GC 单独回收&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;函数返回后，这一帧栈空间整体失效，所以栈内存的管理成本非常低。&lt;/p&gt;
&lt;h4&gt;2. 堆（heap）&lt;/h4&gt;
&lt;p&gt;堆一般存放：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生命周期超过当前函数的对象&lt;/li&gt;
&lt;li&gt;大对象&lt;/li&gt;
&lt;li&gt;不能确定何时释放的对象&lt;/li&gt;
&lt;li&gt;被多个地方共享引用的对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;堆的特点是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;生命周期不规则&lt;/li&gt;
&lt;li&gt;分配比栈复杂&lt;/li&gt;
&lt;li&gt;必须靠 GC 判断“谁还活着，谁已经死了”&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 全局区 / 静态数据区&lt;/h4&gt;
&lt;p&gt;这里通常存放：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全局变量&lt;/li&gt;
&lt;li&gt;常量相关数据&lt;/li&gt;
&lt;li&gt;程序运行期间长期存在的数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这部分一般也会作为 GC 的根对象来源之一。&lt;/p&gt;
&lt;h3&gt;五、Go 如何决定对象放栈上还是堆上&lt;/h3&gt;
&lt;p&gt;这一步靠的是逃逸分析（escape analysis）。&lt;/p&gt;
&lt;p&gt;核心判断可以简单理解为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果一个对象只在当前函数内部使用，且不会在函数返回后继续被引用，那么尽量放栈上&lt;/li&gt;
&lt;li&gt;如果一个对象会“逃出”当前函数作用域，那么放到堆上&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func f() *int {
	x := 10
	return &amp;amp;x
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里 &lt;code&gt;x&lt;/code&gt; 不能放在栈上，因为函数返回后外部还要继续使用它，所以它会逃逸到堆上。&lt;/p&gt;
&lt;p&gt;再看一个不逃逸的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func sum() int {
	x := 10
	y := 20
	return x + y
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里 &lt;code&gt;x&lt;/code&gt; 和 &lt;code&gt;y&lt;/code&gt; 一般都可以放在栈上，因为它们的生命周期只在当前函数内。&lt;/p&gt;
&lt;p&gt;这体现了 Go 的一个重要设计思想：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;能不用 GC 的地方，尽量不要让 GC 介入。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是说，Go 并不是“所有对象都扔给垃圾回收器”，而是先用编译器尽量把对象留在栈上，把 GC 压力降下来。&lt;/p&gt;
&lt;h3&gt;六、Go GC 的核心目标&lt;/h3&gt;
&lt;p&gt;GC 的本质问题只有一句话：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如何找出“还活着的对象”，然后回收其余对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Go 主要采用的是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可达性分析（reachability analysis）&lt;/li&gt;
&lt;li&gt;标记清扫（mark-sweep）&lt;/li&gt;
&lt;li&gt;三色标记（tri-color marking）&lt;/li&gt;
&lt;li&gt;并发回收（concurrent GC）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;它不是简单的“引用计数”。&lt;/p&gt;
&lt;p&gt;因为引用计数虽然直观，但有明显问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;循环引用难处理&lt;/li&gt;
&lt;li&gt;每次赋值都要更新计数，运行期开销大&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Go 选择的是从一组“根对象”出发，沿着引用关系向下遍历：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;能走到的对象，说明还活着&lt;/li&gt;
&lt;li&gt;走不到的对象，说明已经不可达，可以回收&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;七、什么是 GC Root&lt;/h3&gt;
&lt;p&gt;GC 不是从整个内存胡乱扫描，而是从一批根对象开始。&lt;/p&gt;
&lt;p&gt;常见根对象包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前 goroutine 栈上的活动变量&lt;/li&gt;
&lt;li&gt;全局变量&lt;/li&gt;
&lt;li&gt;寄存器中的引用&lt;/li&gt;
&lt;li&gt;运行时自己维护的重要对象&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些根对象相当于“已知还活着的起点”。&lt;/p&gt;
&lt;p&gt;从这些起点出发，所有能顺着指针找到的对象都算存活对象。&lt;/p&gt;
&lt;h3&gt;八、Go 的基本回收算法：标记清扫&lt;/h3&gt;
&lt;p&gt;最直观的思路分两步：&lt;/p&gt;
&lt;h4&gt;1. 标记（Mark）&lt;/h4&gt;
&lt;p&gt;从 GC Root 开始遍历对象图，把所有还能到达的对象打上“存活”标记。&lt;/p&gt;
&lt;h4&gt;2. 清扫（Sweep）&lt;/h4&gt;
&lt;p&gt;遍历堆，把没有被标记的对象回收掉，把空间重新挂回空闲链表或者归还给分配器使用。&lt;/p&gt;
&lt;p&gt;伪代码可以写成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mark(rootSet):
    worklist = rootSet
    while worklist not empty:
        obj = worklist.pop()
        if obj.marked:
            continue
        obj.marked = true
        for each child in obj.references:
            worklist.push(child)

sweep(heap):
    for each obj in heap:
        if obj.marked:
            obj.marked = false
        else:
            free(obj)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个思路已经能工作，但有一个现实问题：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;如果程序一边运行，一边修改对象引用关系，那 GC 扫描出来的结果可能不一致。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这就引出了三色标记和写屏障。&lt;/p&gt;
&lt;h3&gt;九、三色标记法在解决什么问题&lt;/h3&gt;
&lt;p&gt;三色标记是为了在“程序继续运行”的同时，仍然能正确完成标记。&lt;/p&gt;
&lt;p&gt;可以把对象分成三类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;白色：还没被扫描到，默认认为可能是垃圾&lt;/li&gt;
&lt;li&gt;灰色：已经发现这个对象了，但它引用的子对象还没扫描完&lt;/li&gt;
&lt;li&gt;黑色：这个对象和它的子对象都扫描处理过了&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;初始时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;根对象先变灰&lt;/li&gt;
&lt;li&gt;其余对象先看作白色&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;处理过程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;取出一个灰色对象&lt;/li&gt;
&lt;li&gt;扫描它指向的所有对象&lt;/li&gt;
&lt;li&gt;白色子对象变成灰色&lt;/li&gt;
&lt;li&gt;当前对象自己变成黑色&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;直到没有灰色对象，标记阶段结束。此时仍然是白色的对象，就是不可达对象。&lt;/p&gt;
&lt;p&gt;伪代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;init:
    for obj in heap:
        obj.color = white
    for root in roots:
        shade(root)   // root -&amp;gt; gray

mark:
    while grayQueue not empty:
        obj = grayQueue.pop()
        for child in obj.references:
            if child.color == white:
                shade(child)
        obj.color = black
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;shade(obj):
    if obj.color == white:
        obj.color = gray
        grayQueue.push(obj)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;十、为什么并发标记会出错&lt;/h3&gt;
&lt;p&gt;假设 GC 正在执行，某个对象 &lt;code&gt;A&lt;/code&gt; 已经被扫描完，变成黑色；另一个对象 &lt;code&gt;B&lt;/code&gt; 还没被扫描，还是白色。&lt;/p&gt;
&lt;p&gt;这时用户程序做了两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把 &lt;code&gt;A -&amp;gt; B&lt;/code&gt; 这条引用删掉&lt;/li&gt;
&lt;li&gt;又把某个已扫描对象到 &lt;code&gt;B&lt;/code&gt; 的唯一可达路径打断&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果 GC 没注意到这个变化，&lt;code&gt;B&lt;/code&gt; 可能仍然是白色，最后被错误回收，但程序其实后面还可能会用到它。&lt;/p&gt;
&lt;p&gt;更抽象地说，并发标记最怕破坏这个不变量：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;黑色对象不能直接指向白色对象。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;一旦出现“黑指向白”，就可能漏标。&lt;/p&gt;
&lt;h3&gt;十一、Go 如何解决并发标记问题：写屏障&lt;/h3&gt;
&lt;p&gt;写屏障（write barrier）本质上是在“指针写入”这一刻，顺手通知 GC。&lt;/p&gt;
&lt;p&gt;例如程序执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p.next = q
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在普通赋值之外，运行时会额外做一些标记辅助逻辑，避免 &lt;code&gt;q&lt;/code&gt; 被漏掉。&lt;/p&gt;
&lt;p&gt;可以用简化伪代码表示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;writePointer(slot, newObj):
    if gcMarking:
        shade(newObj)
    *slot = newObj
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;真实实现更复杂，但核心思想就是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用户程序改引用关系时&lt;/li&gt;
&lt;li&gt;GC 也同步得到信息&lt;/li&gt;
&lt;li&gt;从而维持三色标记的正确性&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Go 使用写屏障的目的，不是让 GC 更“聪明”，而是让 GC 可以和用户程序并发运行，同时把 STW 时间压缩到很短。&lt;/p&gt;
&lt;h3&gt;十二、STW 是什么，Go 为什么还需要它&lt;/h3&gt;
&lt;p&gt;STW（Stop The World）就是暂停用户程序，让 GC 在一个相对稳定的状态下做某些关键操作。&lt;/p&gt;
&lt;p&gt;很多人会误以为“并发 GC 就完全不会暂停程序”，这不对。更准确的说法是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Go 的目标不是消灭暂停，而是把暂停压缩到尽可能短。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Go 仍然会有一些短暂停顿，常见用途包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;启动标记阶段前做状态切换&lt;/li&gt;
&lt;li&gt;重新扫描某些根对象&lt;/li&gt;
&lt;li&gt;结束标记时完成收尾同步&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;设计思想很明确：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;大部分重活并发做&lt;/li&gt;
&lt;li&gt;少量必须一致性的步骤用短 STW 完成&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这是一种非常工程化的取舍。&lt;/p&gt;
&lt;h3&gt;十三、Go 为什么不强依赖传统分代 GC&lt;/h3&gt;
&lt;p&gt;很多语言会使用“分代 GC”：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新生代：对象大多朝生夕死，频繁回收&lt;/li&gt;
&lt;li&gt;老年代：存活更久，较少回收&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这是因为大量应用符合“多数对象很快死亡”的经验规律。&lt;/p&gt;
&lt;p&gt;但 Go 长期没有把传统分代 GC 作为核心方案，背后有几层现实考虑：&lt;/p&gt;
&lt;h4&gt;1. Go 的对象分配速度本来就很快&lt;/h4&gt;
&lt;p&gt;Go 运行时对小对象分配做了大量优化，很多短命对象即使频繁创建，成本也未必高到需要典型分代结构。&lt;/p&gt;
&lt;h4&gt;2. Go 强调低延迟&lt;/h4&gt;
&lt;p&gt;分代 GC 往往意味着更多屏障、更复杂的代际管理和对象晋升逻辑。Go 更希望先把并发标记清扫做扎实，把暂停时间控制好。&lt;/p&gt;
&lt;h4&gt;3. Go 有大量栈对象和短生命周期协程&lt;/h4&gt;
&lt;p&gt;很多临时数据压根不会进堆，而是通过逃逸分析留在栈上。这样一来，堆上的“年轻垃圾”压力本身就被削弱了。&lt;/p&gt;
&lt;p&gt;所以 Go 的路线可以理解为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先减少必须进入 GC 的对象数量&lt;/li&gt;
&lt;li&gt;再把堆回收设计成并发、低暂停&lt;/li&gt;
&lt;li&gt;用简单且稳定的模型服务工程实践&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;十四、Go GC 的完整流程可以怎么理解&lt;/h3&gt;
&lt;p&gt;可以把一次 GC 粗略理解成下面几个阶段：&lt;/p&gt;
&lt;h4&gt;1. 准备阶段&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;打开 GC 周期&lt;/li&gt;
&lt;li&gt;开启写屏障&lt;/li&gt;
&lt;li&gt;进行短暂 STW，确保状态切换一致&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;2. 并发标记阶段&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;从根对象出发扫描&lt;/li&gt;
&lt;li&gt;工作线程不断处理灰色对象&lt;/li&gt;
&lt;li&gt;用户程序继续运行&lt;/li&gt;
&lt;li&gt;新产生的引用变更由写屏障辅助修正&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;3. 标记终止阶段&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;再次短暂 STW&lt;/li&gt;
&lt;li&gt;处理剩余必须同步完成的工作&lt;/li&gt;
&lt;li&gt;确认标记结束&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4. 清扫阶段&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;回收未标记对象&lt;/li&gt;
&lt;li&gt;把可复用内存交还分配器&lt;/li&gt;
&lt;li&gt;为下一轮分配服务&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果写成更完整一点的伪代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gcCycle():
    stopTheWorld()
    enableWriteBarrier()
    scanRootsAndShade()
    startWorld()

    while grayQueue not empty:
        obj = grayQueue.pop()
        scan(obj)

    stopTheWorld()
    flushRemainingWork()
    disableWriteBarrierWhenSafe()
    startWorld()

    sweepUnmarkedObjects()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;scan(obj)&lt;/code&gt; 可以进一步理解为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;scan(obj):
    for each child in obj.references:
        shade(child)
    obj.color = black
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;十五、从工程视角理解 Go GC 的设计思想&lt;/h3&gt;
&lt;p&gt;Go 的 GC 不是“理论上最优”，而是“工程上足够稳、足够快、足够简单”。&lt;/p&gt;
&lt;p&gt;可以总结成几条主线：&lt;/p&gt;
&lt;h4&gt;1. 优先减少 GC 必须处理的对象&lt;/h4&gt;
&lt;p&gt;通过逃逸分析，让尽可能多的对象留在栈上。&lt;/p&gt;
&lt;p&gt;也就是说，Go 不是先想着“怎么把垃圾回收做得更猛”，而是先想着“能不能别产生那么多需要 GC 的堆对象”。&lt;/p&gt;
&lt;h4&gt;2. 堆对象一旦需要回收，就用可达性分析保证正确性&lt;/h4&gt;
&lt;p&gt;不靠程序员手动释放，也不依赖脆弱的引用计数，而是从根出发扫描对象图。&lt;/p&gt;
&lt;h4&gt;3. 采用三色标记，是为了支持并发&lt;/h4&gt;
&lt;p&gt;并发标记的关键不是“边扫边跑”这么简单，而是必须维持正确性。三色模型让运行时有办法描述对象状态，并配合写屏障修复并发期间的引用变化。&lt;/p&gt;
&lt;h4&gt;4. 接受少量 STW，但严格压缩它&lt;/h4&gt;
&lt;p&gt;Go 并不追求“零暂停神话”，而是承认有些阶段必须全局同步，然后把暂停控制在很短时间。&lt;/p&gt;
&lt;h4&gt;5. 选择非移动、低侵入、可预测的方案&lt;/h4&gt;
&lt;p&gt;Go 的堆对象通常不会像某些压缩型 GC 那样频繁搬家。这让运行时、指针语义、与底层系统交互的复杂度更容易控制。&lt;/p&gt;
&lt;p&gt;这也是 Go 很典型的风格：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不追求最花哨&lt;/li&gt;
&lt;li&gt;先保证行为稳定&lt;/li&gt;
&lt;li&gt;再在实现细节上不断优化&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;十六、学习这部分时应该抓住什么主线&lt;/h3&gt;
&lt;p&gt;如果你现在是第一次系统学 Go GC，建议按下面这条主线理解：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;先理解为什么要区分栈和堆&lt;/li&gt;
&lt;li&gt;再理解逃逸分析为什么能减少 GC 压力&lt;/li&gt;
&lt;li&gt;再理解 GC Root 和可达性分析&lt;/li&gt;
&lt;li&gt;再理解标记清扫为什么需要三色标记&lt;/li&gt;
&lt;li&gt;最后理解写屏障为什么让并发 GC 成为可能&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;真正把这条链路串起来之后，你会发现 Go GC 的核心不是某个单独算法名词，而是一整套相互配合的设计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编译器尽量把对象留在栈上&lt;/li&gt;
&lt;li&gt;运行时负责堆对象的并发回收&lt;/li&gt;
&lt;li&gt;写屏障保证并发标记正确&lt;/li&gt;
&lt;li&gt;STW 只保留在必要的同步点&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;十七、一句话总结&lt;/h3&gt;
&lt;p&gt;Go 的垃圾回收设计思想可以概括为：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;先通过逃逸分析减少进入堆的对象，再对堆对象使用并发三色标记清扫，在保证正确性的前提下尽量降低 STW 和程序员的心智负担。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>RPC相关知识点</title><link>https://jinliye.github.io/Blog/posts/rpc/pms-project/</link><guid isPermaLink="true">https://jinliye.github.io/Blog/posts/rpc/pms-project/</guid><pubDate>Wed, 08 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;::github{repo=&quot;JinLiye/RPC&quot;}&lt;/p&gt;
&lt;h2&gt;HTTP和RPC之间的区别是什么？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;严格来说HTTP是一种通信协议，定义的是网络通信的数据交互和通信方式，而RPC是一种调用方式，定义的是服务端和客户端之间的调用关系。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;为什么微服务内部的通信更倾向于RPC&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;HTTP接口虽然通用性好，跨语言方便，但每次调用都要手动拼URL、设Header、处理返回值，写起来啰嗦。而且HTTP/1.1存在队头阻塞问题，单连接只能串行处理请求，高并发场景下需要建立大量连接，10万QPS的场景下这个开销就很可观了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;RPC框架帮你封装好了这些脏活累活。定义好接口，框架自动生成代理类，调用的时候就像调本地方法一样。连接池、序列化、负载均衡、超时重试这些都帮你搞定了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>设计模式相关内容</title><link>https://jinliye.github.io/Blog/posts/java/designp/</link><guid isPermaLink="true">https://jinliye.github.io/Blog/posts/java/designp/</guid><description>该文档用于记录设计模式相关知识点</description><pubDate>Sat, 21 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;谈谈你了解的最常见的几种设计模式，说说他们的应用场景&lt;/h2&gt;
&lt;h3&gt;1. 单例模式&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;全局只需要用到一个实例的时候。比如数据库连接池；配置中心的客户端&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;2. 工厂模式&lt;/h3&gt;
&lt;h3&gt;3. 策略模式&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;支付场景举例：比如支付宝、微信支付、银联支付等，每个渠道的支付逻辑不一样，天然就是不同的策略。定义一个PayService接口，然后不同的支付渠道实现这个接口，客户端只需要调用PayService接口即可，不需要知道具体是哪个支付渠道。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;策略模式消除if-else&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4. 模板方法模式&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;模板方法模式是一种行为型设计模式，它定义了一个算法的骨架，而将一些步骤延迟到子类中。子类可以不改变算法的骨架即可重定义该算法的某些步骤。模板方法模式通常用于在算法的实现中，有一些步骤是通用的，而有一些步骤是可变的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;image-1.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;什么是责任链模式？一般用在什么场景？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;责任链模式是一种行为型设计模式，它定义了一个请求处理链，每个对象都有机会处理这个请求。如果一个对象不能处理这个请求，它会把请求传递给下一个对象。直到有一个对象处理了这个请求为止。
典型场景：
审批流程：比如请假申请，需要先由组长审批，再由经理审批，最后由总监审批&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// 抽象处理器
abstract class Handler {
    protected Handler next;

    public Handler setNext(Handler next) {
        this.next = next;
        return next; // 返回next方便链式调用
    }

    public abstract void handle(int amount);
}

// 组长：500以内
class LeaderHandler extends Handler {
    public void handle(int amount) {
        if (amount &amp;lt;= 500) {
            System.out.println(&quot;组长审批通过：&quot; + amount);
        } else if (next != null) {
            next.handle(amount);
        }
    }
}

// 经理：500-2000
class ManagerHandler extends Handler {
    public void handle(int amount) {
        if (amount &amp;lt;= 2000) {
            System.out.println(&quot;经理审批通过：&quot; + amount);
        } else if (next != null) {
            next.handle(amount);
        }
    }
}

// 使用
Handler chain = new LeaderHandler();
chain.setNext(new ManagerHandler()).setNext(new DirectorHandler());
chain.handle(1500); // 经理审批通过：1500

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;责任链的两种实现形式&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;独占处理（纯责任链）
请求只能被链上的一个节点处理，处理完成后就结束。&lt;/li&gt;
&lt;li&gt;层层过滤（不纯责任链）
请求会经过链上的多个节点，每个节点都可以做点事情。比如Servelet的FilterChain。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;什么是模板方法模式？一般用在什么场景？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;模板方法模式是一种行为设计模式，核心思想是在一个抽象类中定义一个算法的骨架，而将一些步骤延迟到子类中。子类可以不改变算法的骨架即可重定义该算法的某些步骤。
模板方法模式通常用于在算法的实现中，有一些步骤是通用的，而有一些步骤是可变的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// 抽象类
abstract class DataProcessor {
    // 模板方法，定死执行顺序
    public final void process() {
        readData();
        processData();
        writeData();
    }

    protected abstract void readData();    // 子类必须实现
    protected abstract void processData(); // 子类必须实现

    protected void writeData() {           // 默认实现，子类可覆盖
        System.out.println(&quot;Writing data to output.&quot;);
    }
}

// CSV 处理器
class CSVDataProcessor extends DataProcessor {
    @Override
    protected void readData() {
        System.out.println(&quot;Reading data from CSV file.&quot;);
    }

    @Override
    protected void processData() {
        System.out.println(&quot;Processing CSV data.&quot;);
    }
}

// JSON 处理器
class JSONDataProcessor extends DataProcessor {
    @Override
    protected void readData() {
        System.out.println(&quot;Reading data from JSON file.&quot;);
    }

    @Override
    protected void processData() {
        System.out.println(&quot;Processing JSON data.&quot;);
    }
}


## 什么是观察者模式？一般用在什么场景？
&amp;gt; 观察者模式是一种设计模式，它定义了对象之间的一对多依赖关系，当一个对象的状态发生变化时，所有依赖于它的对象都会得到通知并自动更新。

整个模式由四个角色组成：
1. Subject（主题）：被观察的对象，维护观察者列表，提供添加、删除观察者的方法，以及通知观察者的方法。
2. Observer（观察者）：依赖于主题的对象，实现update方法，用于接收主题的通知。
3. ConcreteSubject（具体主题）：具体实现主题接口，维护观察者列表并实现通知观察者的方法。
4. ConcreteObserver（具体观察者）：具体实现观察者接口，实现update方法，用于处理主题的通知。
![alt text](image-2.png)

### 典型的应用场景
1. 事件驱动系统：比如GUI系统中的事件处理，数据库系统中的触发器。
2. 数据发布订阅系统：比如消息队列中的消息发布订阅。
3. 状态机：比如状态机中的状态变化。


## 什么是代理模式？一般用在什么场景？
&amp;gt; 代理模式是一种结构性设计模式，核心思想是在不改变原始对象的前提下，通过一个代理对象来控制原始对象的访问。

代理模式的主要使用场景：
1. 远程代理：比如网络代理，数据库代理。
2. 虚拟代理：比如图片代理，文件代理。
3. 保护代理：比如权限代理，缓存代理。

```java
// 共同接口
interface UserService {
    void save();
}

// 真实类
class UserServiceImpl implements UserService {
    public void save() {
        System.out.println(&quot;保存用户&quot;);
    }
}

// 代理类
class UserServiceProxy implements UserService {
    private UserService target; // 真实对象
    
    public UserServiceProxy(UserService target) {
        this.target = target;
    }

    public void save() {
        System.out.println(&quot;开启事务&quot;); // 增强
        target.save(); // 调用真实方法
        System.out.println(&quot;提交事务&quot;); // 增强
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;两种代理模式&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;静态代理：在编译时就已经确定了代理类。&lt;/li&gt;
&lt;li&gt;动态代理：在运行时动态生成代理类，比如Java的动态代理机制。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class LogHandler implements InvocationHandler {
    private Object target;

    public LogHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(&quot;方法 &quot; + method.getName() + &quot; 开始执行&quot;);
        Object result = method.invoke(target, args);  // 反射调用真实对象
        System.out.println(&quot;方法 &quot; + method.getName() + &quot; 执行结束&quot;);
        return result;
    }
}

// 创建代理对象
UserService proxy = (UserService) Proxy.newProxyInstance(
    UserService.class.getClassLoader(),
    new Class[]{UserService.class},
    new LogHandler(new UserServiceImpl())
);
proxy.save(user);  // 走代理

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;简述简单工厂模式的工作原理。&lt;/h2&gt;
&lt;p&gt;简单工厂模式的核心思想是把对象创建逻辑集中到一个工厂类中，调用方法只需要告诉工厂类需要创建的对象类型，工厂类会根据类型创建对应的对象。
&lt;img src=&quot;image-3.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 产品接口
public interface Database {
    void connect();
}

// 具体产品
public class MySQL implements Database {
    public void connect() {
        System.out.println(&quot;连接 MySQL 数据库&quot;);
    }
}

public class PostgreSQL implements Database {
    public void connect() {
        System.out.println(&quot;连接 PostgreSQL 数据库&quot;);
    }
}

// 简单工厂
public class DatabaseFactory {
    public static Database createDatabase(String type) {
        switch (type) {
            case &quot;mysql&quot;:
                return new MySQL();
            case &quot;postgresql&quot;:
                return new PostgreSQL();
            default:
                throw new IllegalArgumentException(&quot;不支持的数据库类型: &quot; + type);
        }
    }
}

// 客户端使用
Database db = DatabaseFactory.createDatabase(&quot;mysql&quot;);
db.connect();

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;存在的问题&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;如果需要新增产品，需要修改工厂类，违反开闭原则。&lt;/li&gt;
&lt;li&gt;工厂类职责过重，违反单一职责原则。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;解决方案&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;使用配置驱动工厂模式
&lt;img src=&quot;image-4.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;public class ConfigurableFactory {
    private static Map&amp;lt;String, String&amp;gt; typeMapping = new HashMap&amp;lt;&amp;gt;();

    static {
        // 从配置文件加载映射关系
        typeMapping.put(&quot;mysql&quot;, &quot;com.example.MySQL&quot;);
        typeMapping.put(&quot;postgresql&quot;, &quot;com.example.PostgreSQL&quot;);
    }

    public static Database create(String type) {
        String className = typeMapping.get(type);
        return (Database) Class.forName(className).newInstance();
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;简单工厂和工厂方法的区别是什么？&lt;/h3&gt;
&lt;p&gt;简单工厂把所有工厂创建逻辑堆在一个类中，新增产品就要改工厂代码。
工厂方法把创建逻辑分散到各个子类工厂中，新增产品只需要加一个新的子类工厂，符合开闭原则，但是代价是子类的数量会膨胀，每新增一个产品就要多一个工厂类，如果产品类型不多，工厂模式就够用了。&lt;/p&gt;
&lt;h3&gt;工厂模式和抽象工厂模式的区别是什么？&lt;/h3&gt;
&lt;p&gt;这两个都是创建型设计模式，核心目的都是解耦对象创建（不用自己 new 对象，让工厂帮你创建），区别主要在于产品规模、工厂职责、使用场景。
核心区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;工厂模式：生产单一产品（一个工厂只造一种东西）&lt;/li&gt;
&lt;li&gt;抽象工厂模式：生产产品族（一个工厂造一整套相关联的东西）&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;工厂模式 = 单品类工厂
你只需要一种东西，比如：华为工厂 → 只造华为手机；苹果工厂 → 只造苹果手机。一个工厂，只干一件事。&lt;/li&gt;
&lt;li&gt;抽象工厂模式 = 成套设备工厂你需要一整套配套的东西，比如：苹果工厂 → 造苹果手机 + 苹果耳机 + 苹果充电器；华为工厂 → 造华为手机 + 华为耳机 + 华为充电器。一个工厂，造一整套产品，且产品之间必须配套。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;单例模式有哪几种实现，如何保证线程安全？&lt;/h3&gt;
&lt;p&gt;单例模式（Singleton）是创建型设计模式，核心目的是：保证一个类在整个程序中只有一个实例，并提供全局访问点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;饿汉式单例
在类加载时就创建好实例，线程安全。缺点是不管用不用都会占用内存。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class Singleton {
    private static final Singleton instance = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;懒汉式
在第一次使用时才创建实例，线程不安全，需要加锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class Singleton {
    private static Singleton instance;
    private Singleton() {}
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;双重检查锁
在懒汉式基础上加一层判断，减少锁的粒度，提高性能，线程安全。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class Singleton4 {
    // volatile：禁止指令重排，保证多线程可见性
    private static volatile Singleton4 instance;

    private Singleton4() {}

    public static Singleton4 getInstance() {
        // 第一次检查：不加锁，提高效率
        if (instance == null) {
            // 加锁：保证只有一个线程创建实例
            synchronized (Singleton4.class) {
                // 第二次检查：防止多线程同时进入第一层判断
                if (instance == null) {
                    instance = new Singleton4();
                }
            }
        }
        return instance;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;静态内部类
利用静态内部类实现懒加载，线程安全。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public class Singleton {
    private Singleton() {}
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;枚举
枚举类型是线程安全的，并且只会加载一次，实现简单。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;public enum Singleton {
    INSTANCE;
    public void doSomething() {
        System.out.println(&quot;doSomething&quot;);
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Mysql_引擎</title><link>https://jinliye.github.io/Blog/posts/database/mysql_engin/</link><guid isPermaLink="true">https://jinliye.github.io/Blog/posts/database/mysql_engin/</guid><pubDate>Mon, 09 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Mysql_引擎&lt;/h2&gt;
</content:encoded></item><item><title>Mysql基础</title><link>https://jinliye.github.io/Blog/posts/database/mysql_base/</link><guid isPermaLink="true">https://jinliye.github.io/Blog/posts/database/mysql_base/</guid><description>Mysql基础</description><pubDate>Thu, 05 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;数据库的脏读、不可重复读、幻读分别是什么问题？&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;脏读：一个事务读取了另一个事务未提交的数据&lt;/li&gt;
&lt;li&gt;不可重复读：一个事务在读取数据后，另一个事务修改了数据，导致第一个事务读取到的数据不一致&lt;/li&gt;
&lt;li&gt;幻读：一个事务在读取数据后，另一个事务插入了数据，导致第一个事务读取到的数据不一致（强调数据量大变化）
&lt;img src=&quot;image-5.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;数据库的四大隔离级别&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;读未提交（Read Uncommitted）&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;别人还没提交的事务，你都能读到&lt;/li&gt;
&lt;li&gt;会读到脏数据（别人修改了又回滚了）&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;读已提交（Read Committed） &lt;strong&gt;这是Oracle、SQL Server默认的隔离级别&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;只能读到别人已经提交的数据&lt;/li&gt;
&lt;li&gt;解决了脏读问题&lt;/li&gt;
&lt;li&gt;但是会读到不可重复读的问题（同一条 SQL，两次读结果不一样（因为中间别人改了并提交了））&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;可重复读（Repeatable Read） &lt;strong&gt;这是MySQL默认的隔离级别&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;解决了不可重复读问题&lt;/li&gt;
&lt;li&gt;但是会读到幻读的问题（同一条 SQL，两次读结果不一样（因为中间别人插入了数据））&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;串行化（Serializable）&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;解决了幻读问题&lt;/li&gt;
&lt;li&gt;但是性能会非常差&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Mysql事务的二阶段提交是什么？&lt;/h2&gt;
&lt;p&gt;Mysql的二阶段提交是为了解决Servcer层和引擎层的事务一致性问题
整个过程分为两步来走：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;prepare阶段&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;事务提交时候，InnoDB先将修改写入redo log中，其状态标志为Prepared，表示我准备好了，但是还没有提交&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;commit阶段&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;redo log写完，Server层把操作写入bin log中，bin log落盘成功，再通知InnoDB把redo log中的状态改为commit，整个事务才算提交完成。
&lt;img src=&quot;image-10.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;什么是Mysql中的主从同步机制？它是如何实现的？&lt;/h2&gt;
&lt;p&gt;Mysql的主从同步的核心是binlog复制，主库把写日志写到binlog日志中，从库拉过来执行一遍就实现了主从同步。主要涉及到三个线程之间的配合。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主库的dump线程，监听binlog变更，有新内容就推送到从库&lt;/li&gt;
&lt;li&gt;从库的I/O线程，拉取主库数据，把收到的binlog写道本地的relaylog&lt;/li&gt;
&lt;li&gt;从库的SQL线程，负责从relay log中读取，逐条执行sql语句
&lt;img src=&quot;image-11.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;如何处理Mysql的主从同步延迟？&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;主从延迟&lt;/strong&gt;是必然存在的，是不可避免的，只能尽量缩短延迟时间或在业务层面做规避。
业务层面的常见处理方案：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;关键业务强制走主库，比如注册完后登录的逻辑，这类操作的频次不高，对主库的压力有限。&lt;/li&gt;
&lt;li&gt;延迟感知。 写操作后记录时间戳，短时间内的读请求走主库，过了延迟时间再走从库（可以使用ThreadLocal来实现延迟感知,或者使用Redis来实现延迟感知）&lt;/li&gt;
&lt;li&gt;二次查询兜底。从库查不到就去主库查。（但是会增加主库的压力，可以作为攻击手段）&lt;/li&gt;
&lt;li&gt;缓存前置。写入主库的同时写入缓存，不过又引入了缓存一致性问题，属于用一个问题换另外一个问题。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;Mysql中的长事务会导致什么问题？&lt;/h2&gt;
&lt;h2&gt;Mysql的乐观锁和悲观锁是什么？&lt;/h2&gt;
&lt;h3&gt;主要的行级锁&lt;/h3&gt;
&lt;p&gt;在 MySQL 中，排他锁（X 锁）和共享锁（S 锁）是行级锁的核心类型（InnoDB 引擎支持），用于控制多事务并发访问数据时的一致性和隔离性。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;共享锁（Shared Lock，S 锁）：相当于给文件加 “只读锁”，多个用户都能加这个锁，只能读不能改。&lt;/li&gt;
&lt;li&gt;排他锁（Exclusive Lock，X 锁）：相当于给文件加 “独占锁”，只有一个用户能加这个锁，既可以读也可以改，其他人既不能改也不能加排他锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;乐观锁和悲观锁&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;乐观锁和悲观锁是两种并发控制思想，本质区别在于对冲突的预期态度不同。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;乐观锁：假设冲突不存在，不会加锁，只在提交时检查是否有冲突。通常使用版本号来实现，读的时候把version一起读出来，更新的时候判断version是否一致。&lt;/li&gt;
&lt;li&gt;悲观锁：假设冲突存在，会加锁，其他事务不能并发访问数据。
&lt;img src=&quot;image-12.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Mysql中有哪些锁类型？&lt;/h2&gt;
&lt;p&gt;Mysql InnoDB的锁可以从两个维度来区分：粒度和模式。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;粒度：锁的范围，分为行级锁和表级锁。&lt;/li&gt;
&lt;li&gt;模式：锁的类型，分为共享锁和排他锁。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;行级锁&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;记录锁（Record Lock）：对表中的行进行加锁，只能对这行数据进行读写操作，其他事务不能对这行数据进行读写操作。&lt;/li&gt;
&lt;li&gt;间隙锁（Gap Lock）：对表中的间隙进行加锁，防止其他事务插入数据到这个间隙中，从而避免幻读现象。&lt;/li&gt;
&lt;li&gt;临键锁（Next-Key Lock）：记录锁和间隙锁的组合，对表中的记录和间隙进行加锁，防止其他事务插入数据到这个间隙中，从而避免幻读现象。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;表级锁&lt;/h3&gt;
&lt;p&gt;表锁（Table Lock）：对整个表进行加锁，其他事务不能对表中的任何数据进行读写操作。&lt;/p&gt;
</content:encoded></item><item><title>Java中HashMap相关知识点</title><link>https://jinliye.github.io/Blog/posts/java/hashmap/</link><guid isPermaLink="true">https://jinliye.github.io/Blog/posts/java/hashmap/</guid><description>该文档用于记录Java中HashMap相关知识点</description><pubDate>Thu, 05 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;HashMap的原理&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;怎么存？
&lt;ul&gt;
&lt;li&gt;先计算key的hashCode值（&quot;apple&quot;.hashCode()=97099），后进行哈希扰动（(key == null) ? 0 : (h = key.hashCode()) ^ (h &amp;gt;&amp;gt;&amp;gt; 16)）,其目的在于让让高 16 位和低 16 位混合，保证高位也能影响最终结果，让哈希结果更平均&lt;/li&gt;
&lt;li&gt;再对hashCode值进行取模运算，得到数组下标（hash &amp;amp; (n-1)），其中n是2的幂次，等价于 hash % n&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;冲突了怎么办？
&lt;ul&gt;
&lt;li&gt;链表串联&lt;/li&gt;
&lt;li&gt;JDK8做了优化，如果超过8个则用红黑树串起来&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;扩容机制
&lt;ul&gt;
&lt;li&gt;数组长度是有限的，存多了会拥挤，hashmap有个负载因子（默认0.75），当存的元素超过数组长度*负载因子时，就会触发扩容。然后把所有的数据重新放到新数组中，这个操作叫rehash，比较耗费性能，最好初始时预估好容量。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;image.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;HashMap线程安全性问题&lt;/h1&gt;
&lt;ol&gt;
&lt;li&gt;数据覆盖
&lt;blockquote&gt;
&lt;p&gt;HashMap 的 put 操作不是原子性的，核心步骤（计算下标→检查桶位→插入节点）被拆分成了多个步骤，多线程切换执行时会 “插队”：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ul&gt;
&lt;li&gt;多线程同时执行 put 操作时，可能导致同一个 key 对应的 value 被错误覆盖。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;扩容时的死循环（JDK 1.7 经典问题）
&lt;ul&gt;
&lt;li&gt;JDK 1.7 中 HashMap 扩容（resize）时采用 “头插法” 迁移链表节点，多线程扩容会导致链表成环，后续调用 get 方法时会陷入死循环（CPU 100%）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;数据丢失 / 查询结果不一致&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;为什么不设计成线程安全的？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;HashMap 的设计目标是极致的单线程性能，如果加入锁等线程安全机制，会大幅降低执行效率（比如锁竞争、内存屏障等）&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;如何解决？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;使用ConcurrentHashMap，它是HashMap的线程安全版本，内部采用了分段锁（Segment）机制，每个Segment都是一个独立的HashMap，不同线程可以并发操作不同的Segment，从而提高了并发性能。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;JDK1.7和JDK1.8的ConcurrentHashMap区别&lt;/h2&gt;
&lt;p&gt;两个版本的核心区别在于锁的力度。&lt;/p&gt;
&lt;h3&gt;JDK1.7&lt;/h3&gt;
&lt;p&gt;采用分段锁设计，把整个数组分成16个Segment，每个Segment里都是一个独立的hashmap加一个ReentrantLock（可重入锁），不同线程可以并发操作不同的Segment，从而提高了并发性能。&lt;/p&gt;
&lt;h3&gt;JDK1.8&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;取消了Segment，采用了CAS+Synchronized（乐观锁+悲观锁）的机制。锁力度细化到了每个槽位，插入时先尝试CAS无锁插入到对应位置，真正冲突了才Sysnchronized加锁。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>RuoYi_vue</title><link>https://jinliye.github.io/Blog/posts/java/ruoyi_vue/</link><guid isPermaLink="true">https://jinliye.github.io/Blog/posts/java/ruoyi_vue/</guid><description>该文档用于记录RuoYi_vue框架的学习过程，源码解析及二次开发</description><pubDate>Fri, 16 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;::github{repo=&quot;yangzongzhuan/RuoYi-Vue3&quot;}&lt;/p&gt;
</content:encoded></item><item><title>Mysql_索引</title><link>https://jinliye.github.io/Blog/posts/database/mysql_index/</link><guid isPermaLink="true">https://jinliye.github.io/Blog/posts/database/mysql_index/</guid><description>该文档用于记录Mysql 索引的学习过程，源码分析</description><pubDate>Fri, 16 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;什么是索引？&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;一种帮助Mysql提高查询效率的数据结构&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;索引的优缺点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;优点：
&lt;ul&gt;
&lt;li&gt;提高查询效率&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;缺点：
&lt;ul&gt;
&lt;li&gt;占用额外的磁盘空间&lt;/li&gt;
&lt;li&gt;增加插入、更新和删除操作的成本（需要对索引进行维护）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;索引的分类&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;a.主键索引&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置主键后会自动创建的索引（聚簇索引）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;b.单值索引（单列索引、普通索引）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;给表中的某个字段单独加一个索引（可有多个）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;c.唯一索引&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;给表中的某个字段单独加一个索引（可有多个），索引值必须唯一（不允许重复），但允许有空值（NULL）。&lt;/li&gt;
&lt;li&gt;InnoDB存储引擎中允许多个NULL，NULL代表未知，NULL！=NULL（与其他任何值都不相等）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;d.组合索引&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;给表中的多个字段组合起来加一个索引（只能有一个），用于提高多字段查询的效率。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;e.全文索引&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;给表中的某个字段单独加一个索引（只能有一个），用于全文搜索。用于CHAR、VARCHAR、TEXT类型的字段。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;索引的创建&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;a.主键索引&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建表时设置主键，会自动创建主键索引。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;b.单值索引&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;建表时建立索引：&lt;pre&gt;&lt;code&gt;CREATE TABLE table_name (
  column_name data_type primary key,
  INDEX index_name (column_name)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;使用CREATE INDEX语句创建单值索引。&lt;pre&gt;&lt;code&gt;CREATE INDEX index_name ON table_name (column_name);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;c.唯一索引&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;建表时建立索引：&lt;pre&gt;&lt;code&gt;CREATE TABLE table_name (
  column_name data_type unique,
  INDEX index_name (column_name)
);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;使用CREATE UNIQUE INDEX语句创建唯一索引：&lt;pre&gt;&lt;code&gt;CREATE UNIQUE INDEX index_name ON table_name (column_name);
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;d.组合索引&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用CREATE INDEX语句创建组合索引。&lt;/li&gt;
&lt;li&gt;左前缀索引：
&lt;ul&gt;
&lt;li&gt;最佳左前缀原则&lt;/li&gt;
&lt;li&gt;mysql引擎为了更好的利用索引，会动态调整查询字段顺序以利用索引&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- 创建索引 --&amp;gt;
CREATE INDEX idx_user ON user (country, city, age);
&amp;lt;!-- 有限利用索引 --&amp;gt;
SELECT * FROM user WHERE country = &apos;中国&apos; AND city = &apos;北京&apos; AND age = 18;
&amp;lt;!-- 动态排序利用索引结构 --&amp;gt;
SELECT * FROM user WHERE city = &apos;北京&apos; AND country = &apos;中国&apos;;
&amp;lt;!-- 缺少最左列，索引失效 --&amp;gt;
SELECT * FROM user WHERE city = &apos;北京&apos;;
&amp;lt;!-- 中间断档查询，只能有效利用country索引，age列无法参与索引过滤 --&amp;gt;
SELECT * FROM user WHERE country = &apos;中国&apos; AND age = 18;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;e.全文索引&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用CREATE FULLTEXT INDEX语句创建全文索引。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;索引的底层原理（B+树）&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;image-1.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;B+树是一种自平衡的树形数据结构，它是一种多路搜索树，每个节点可以有多个子节点，但每个节点只有两个子节点时，称为二叉树。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;B+树的特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有叶子节点都在同一层，&lt;strong&gt;叶子节点存储数据，非叶子节点存储索引&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;非叶子节点中的索引指向叶子节点中的数据。&lt;/li&gt;
&lt;li&gt;非叶子节点中的索引按照升序排列，叶子节点中的数据按照升序排列。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;聚簇索引和非聚簇索引&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;聚簇索引：将数据存储与索引放到了一起，索引结构的叶子节点保存了行数据&lt;/li&gt;
&lt;li&gt;非聚簇索引：将数据与索引分开存储，索引结构的叶子节点保存的是指向实际数据的指针&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;非聚簇索引一定是二次查找&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;为什么非聚簇索引存储的是主键值而不是实际的元素的位置&lt;/strong&gt;？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当你插入、删除、更新数据，或表发生碎片整理（OPTIMIZE TABLE）时，数据行可能会被移动到磁盘的其他位置（比如页分裂、页合并）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果非聚簇索引存的是物理位置：数据一移动，所有关联的非聚簇索引都要同步更新这个物理地址，维护成本极高，且极易出现索引和数据地址不匹配的错误（索引失效）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果非聚簇索引存的是主键值：主键是唯一且稳定的（InnoDB 建议用自增主键，几乎不会修改），无论数据行的物理位置怎么变，主键值不变 → 非聚簇索引无需修改，天然保证索引的有效性。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;使用聚簇索引的优势&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同一页会有多条行数据，访问同一数据页不同记录时，已经加载到Buffer中，再次访问时，会在内存中完成访问，不必访问磁盘。&lt;/li&gt;
&lt;li&gt;辅助素引的叶子节点，存储主键值，而不是数据的存放地址。好处是当行数据放生变化时，素引树的节点也需要分裂变化;或者是我们需要查找的进据，在上一次I0读写的内存中没有，需要发生一次新的操作时，可以避免对铺助素引的维护工作，只需要维护聚簇素引树就好了。另一个好处是，因为辅助素引存放的是主键值，减少了辅助素引占用的存储空间大小。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;使用聚簇索引的注意点&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用举聚簇索引的时候最好不用uuid，因为uuid的值是随机值，在插入的时候，为了维护B+树的特性，可能会造成页分裂，页合并，影响性能。&lt;/li&gt;
&lt;li&gt;使用聚簇索引的时候最好使用自增的id，因为自增的id是顺序的，对索引树的维护成本较低。
&lt;img src=&quot;image.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;什么情况下不会使用索引&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Like关键字以%开头不会使用索引&lt;/li&gt;
&lt;li&gt;多列索引遵循最左匹配原则&lt;/li&gt;
&lt;li&gt;使用OR关键字时，左右有一个不是索引，在查询时将不会使用索引&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;什么是mysql的覆盖索引&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;覆盖索引：指的是查询语句中使用的索引，包含了查询语句中需要的所有字段，而不需要再去访问数据页。&lt;/li&gt;
&lt;li&gt;覆盖索引的优势：
&lt;ul&gt;
&lt;li&gt;减少了磁盘I/O次数，提高了查询效率。&lt;/li&gt;
&lt;li&gt;减少了内存占用，提高了查询效率。
&lt;img src=&quot;image-4.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Mysql的索引下推(ICP)是什么？&lt;/h2&gt;
&lt;p&gt;索引下推是 MySQL 在 InnoDB 存储引擎中对索引扫描优化的一种技术。&lt;/p&gt;
&lt;p&gt;传统索引扫描流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;MySQL 会根据条件找到索引页，拿到 行指针（rowid）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;再去 主表（heap） 中读取整行数据。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在服务器端对 WHERE 的全部条件再次过滤。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;存在问题：
如果索引匹配很多行，但实际符合最终条件的只有少数，MySQL 会读很多不必要的行，I/O 成本高。&lt;/p&gt;
&lt;p&gt;ICP 优化后流程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;MySQL 尽量把 WHERE 条件在索引层过滤掉，只把可能符合条件的行的指针返回到存储引擎，再去读主表。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;这样可以减少回表（访问主表）的次数，提高性能。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;简单理解：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“把能在索引上做的过滤尽量提前，减少访问主表的数据量。”&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Mysql的索引是否越多越好？为什么&lt;/h2&gt;
&lt;p&gt;索引过多带来的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;索写性能急剧下降&lt;/li&gt;
&lt;li&gt;占用更多的磁盘空间&lt;/li&gt;
&lt;li&gt;优化器选择困难&lt;/li&gt;
&lt;li&gt;DDL操作变慢（如添加索引、删除索引、重建索引、添加字段等）
索引建立的原则：&lt;/li&gt;
&lt;li&gt;按需建立索引，不预防性建立索引。先跑业务，等慢查询出来后，再针对性的建立索引、&lt;/li&gt;
&lt;li&gt;优先考虑联合索引&lt;/li&gt;
&lt;li&gt;单表索引控制再5个以内，联合索引字段不超过5个&lt;/li&gt;
&lt;li&gt;对于写多读少的表，索引越少越好&lt;/li&gt;
&lt;li&gt;定期清理无效索引&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>PMS-project</title><link>https://jinliye.github.io/Blog/posts/project/pms-project/</link><guid isPermaLink="true">https://jinliye.github.io/Blog/posts/project/pms-project/</guid><pubDate>Mon, 12 Jan 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;Info&lt;/h2&gt;
&lt;p&gt;::github{repo=&quot;Event-AHU/Open_VLTrack&quot;}&lt;/p&gt;
</content:encoded></item></channel></rss>