[{"data":1,"prerenderedAt":6565},["ShallowReactive",2],{"java-interview-all":3,"java-interview-search":5876},[4,646,1106,1480,1704,2048,2496,2763,2997,3225,3669,4142,4447,4913,5232,5621],{"id":5,"title":6,"body":7,"category":632,"cover":633,"date":634,"description":635,"extension":636,"group":637,"meta":638,"navigation":639,"order":640,"path":641,"seo":642,"sitemap":643,"stem":644,"__hash__":645},"javaInterview\u002Fjava-interview\u002Fcollections.md","Java 集合：选型、扩容与并发容器",{"type":8,"value":9,"toc":612},"minimark",[10,14,135,147,155,167,187,194,209,220,242,254,265,276,279,283,301,316,325,332,339,342,357,369,385,402,411,422,437,442,456,460,474,492,499,502,557,575,578],[11,12,13],"p",{},"集合题的失分点通常不是「不知道 API」，而是选型理由说不清、边界条件记混。本文把常用实现放在同一张表里对照，再逐题拆开。",[15,16,17,33],"table",{},[18,19,20],"thead",{},[21,22,23,27,30],"tr",{},[24,25,26],"th",{},"需求",[24,28,29],{},"首选",[24,31,32],{},"避免",[34,35,36,54,70,85,100,116],"tbody",{},[21,37,38,42,48],{},[39,40,41],"td",{},"随机访问、尾部追加",[39,43,44],{},[45,46,47],"code",{},"ArrayList",[39,49,50,51],{},"头部频繁插入用 ",[45,52,53],{},"LinkedList",[21,55,56,59,64],{},[39,57,58],{},"栈、队列、双端队列",[39,60,61],{},[45,62,63],{},"ArrayDeque",[39,65,66,67,69],{},"用 ",[45,68,53],{}," 充当队列",[21,71,72,75,80],{},[39,73,74],{},"键值查找，无需排序",[39,76,77],{},[45,78,79],{},"HashMap",[39,81,82,83],{},"多线程共享时用 ",[45,84,79],{},[21,86,87,90,95],{},[39,88,89],{},"保留插入顺序",[39,91,92],{},[45,93,94],{},"LinkedHashMap",[39,96,97,98],{},"为排序而用 ",[45,99,94],{},[21,101,102,105,110],{},[39,103,104],{},"按 key 排序 \u002F 范围查询",[39,106,107],{},[45,108,109],{},"TreeMap",[39,111,112,113,115],{},"大数据量下用 ",[45,114,109],{}," 做计数",[21,117,118,121,126],{},[39,119,120],{},"高并发读写",[39,122,123],{},[45,124,125],{},"ConcurrentHashMap",[39,127,128,131,132],{},[45,129,130],{},"Hashtable","、",[45,133,134],{},"Collections.synchronizedMap",[136,137,139,140,131,142,131,144,146],"h2",{"id":138},"_1-arraylistlinkedlistarraydeque-怎么选","1. ",[45,141,47],{},[45,143,53],{},[45,145,63],{}," 怎么选？",[11,148,149,151,152,154],{},[45,150,47],{}," 基于连续数组，随机访问 O(1)，尾部追加摊还 O(1)，中间插入要移动元素；",[45,153,53],{}," 是双向链表，头尾操作 O(1)，但按下标访问是 O(n)，而且每个节点都有额外的对象头和前后指针，缓存局部性差。",[11,156,157,158,160,161,163,164,166],{},"实践结论：绝大多数场景选 ",[45,159,47],{},"。即使频繁在头部插入，",[45,162,63],{}," 的环形数组通常也比 ",[45,165,53],{}," 快；只有在持有节点引用做大量中间删除时，链表才有优势。",[11,168,169,171,172,175,176,179,180,182,183,186],{},[45,170,63],{}," 不允许 ",[45,173,174],{},"null","，容量按 2 倍增长，作为栈比 ",[45,177,178],{},"Stack"," 更合适，因为 ",[45,181,178],{}," 继承自 ",[45,184,185],{},"Vector","，每个方法都加锁。",[136,188,190,191,193],{"id":189},"_2-arraylist-的扩容过程是怎样的","2. ",[45,192,47],{}," 的扩容过程是怎样的？",[11,195,196,197,200,201,204,205,208],{},"默认构造出来的是空数组，第一次 ",[45,198,199],{},"add"," 才扩到 10。之后容量不够时按 ",[45,202,203],{},"oldCapacity + (oldCapacity >> 1)"," 扩到约 1.5 倍，再用 ",[45,206,207],{},"Arrays.copyOf"," 复制到新数组。",[11,210,211,212,215,216,219],{},"扩容是 O(n) 的复制，所以能预估大小时应该用 ",[45,213,214],{},"new ArrayList\u003C>(size)"," 一次性分配，避免多次搬移。",[45,217,218],{},"trimToSize()"," 可以回收多余容量，但会再复制一次，不要频繁调用。",[11,221,222,223,226,227,230,231,234,235,237,238,241],{},"注意 ",[45,224,225],{},"subList()"," 返回的是视图而非拷贝，原列表结构修改后视图会抛 ",[45,228,229],{},"ConcurrentModificationException","；",[45,232,233],{},"Arrays.asList()"," 返回定长列表，",[45,236,199],{}," 会抛异常，但 ",[45,239,240],{},"set"," 可以改。",[136,243,245,246,131,248,131,250,131,252,146],{"id":244},"_3-hashmaplinkedhashmaptreemapconcurrenthashmap-怎么选","3. ",[45,247,79],{},[45,249,94],{},[45,251,109],{},[45,253,125],{},[11,255,256,258,259,261,262,264],{},[45,257,79],{}," 用哈希表，平均 O(1)，不保证顺序；",[45,260,94],{}," 在哈希表基础上加了双向链表，可保留插入顺序或访问顺序，适合做 LRU；",[45,263,109],{}," 基于红黑树，O(log n)，支持排序和范围查询，但 key 必须可比较。",[11,266,267,269,270,272,273,275],{},[45,268,125],{}," 用 CAS 加桶级锁保证并发安全，读操作基本无锁，适合多线程共享的缓存和计数场景。它不允许 ",[45,271,174],{}," 键值，这一点和 ",[45,274,79],{}," 不同。",[11,277,278],{},"选择顺序通常是：先问要不要并发，再问要不要顺序，最后才看单次操作复杂度。很多「用 TreeMap 排序」的需求，其实在查询时对结果排一次序更划算。",[136,280,282],{"id":281},"_4-什么是-fail-fast遍历时删除为什么会抛异常","4. 什么是 fail-fast？遍历时删除为什么会抛异常？",[11,284,285,131,287,289,290,293,294,297,298,300],{},[45,286,47],{},[45,288,79],{}," 等非并发集合在结构修改时会改变 ",[45,291,292],{},"modCount","；迭代器创建时记下期望值，每次 ",[45,295,296],{},"next()"," 都检查两者是否一致，不一致就抛 ",[45,299,229],{},"。",[11,302,303,304,307,308,311,312,315],{},"所以 ",[45,305,306],{},"for (String s : list) list.remove(s)"," 会失败，正确写法是 ",[45,309,310],{},"iterator.remove()","，或者用 ",[45,313,314],{},"removeIf","，后者内部同样走了迭代器。单线程下这是主动检测；多线程下它只是「尽力而为」，不能当并发保护使用。",[11,317,318,320,321,324],{},[45,319,125],{}," 的迭代器是弱一致的：不抛异常，但不保证看到遍历开始后的所有修改。",[45,322,323],{},"CopyOnWriteArrayList"," 的迭代器基于快照，同样不抛异常，也看不到快照之后的更新。",[136,326,328,329,331],{"id":327},"_5-copyonwritearraylist-适合什么场景代价是什么","5. ",[45,330,323],{}," 适合什么场景？代价是什么？",[11,333,334,335,338],{},"写操作时复制整个数组，在副本上修改，再用 ",[45,336,337],{},"ReentrantLock"," 保护发布；读操作完全无锁，直接读当前数组引用。",[11,340,341],{},"它适合「读多写极少、且读方不能接受加锁」的场景，例如监听器列表、黑白名单、路由配置。写操作是 O(n) 复制，元素多或写频繁时性能会很差，内存峰值也会翻倍。",[11,343,344,345,230,347,349,350,353,354,356],{},"它的迭代器是快照式的：遍历期间看不到新写入，也不会抛 ",[45,346,229],{},[45,348,310],{}," 不支持，要删元素得直接调列表的 ",[45,351,352],{},"remove","。如果要求实时一致，应该换 ",[45,355,125],{}," 对应的并发结构或加锁。",[136,358,360,361,364,365,368],{"id":359},"_6-comparable-和-comparator-有什么区别排序要注意什么","6. ",[45,362,363],{},"Comparable"," 和 ",[45,366,367],{},"Comparator"," 有什么区别？排序要注意什么？",[11,370,371,373,374,377,378,380,381,384],{},[45,372,363],{}," 由元素自身实现 ",[45,375,376],{},"compareTo","，定义「自然顺序」，一个类通常只有一种；",[45,379,367],{}," 是外部比较器，可以按需定义多种排序规则，配合 ",[45,382,383],{},"Comparator.comparing(...).thenComparing(...)"," 组合。",[11,386,387,388,391,392,395,396,230,399,401],{},"注意三点：比较器必须满足自反、反对称、传递，否则 ",[45,389,390],{},"TimSort"," 可能抛 ",[45,393,394],{},"IllegalArgumentException: Comparison method violates its general contract","；用减法实现比较可能整数溢出，应该用 ",[45,397,398],{},"Integer.compare",[45,400,174],{}," 值要显式处理。",[11,403,404,364,407,410],{},[45,405,406],{},"Collections.sort",[45,408,409],{},"Arrays.sort"," 对对象数组使用稳定的归并类算法，相等元素的相对顺序会保留；对基本类型数组则使用不稳定但更快的双轴快排。",[136,412,414,415,364,418,421],{"id":413},"_7-listof-和-collectionsunmodifiablelist-有什么区别","7. ",[45,416,417],{},"List.of()",[45,419,420],{},"Collections.unmodifiableList()"," 有什么区别？",[11,423,424,426,427,131,429,131,431,433,434,436],{},[45,425,417],{}," 创建的是真正不可变且独立的列表：内部拷贝了元素，之后原数组或原集合的修改不会影响它，任何 ",[45,428,199],{},[45,430,240],{},[45,432,352],{}," 都抛异常，且不允许 ",[45,435,174],{}," 元素。",[11,438,439,441],{},[45,440,420],{}," 是包装视图：它自己拒绝修改，但底层列表变化时会「透传」出来，因此只读保证是相对的。适合「对外暴露只读视图、内部仍需修改」的场景。",[11,443,444,446,447,449,450,452,453,455],{},[45,445,233],{}," 又是另一种：返回定长视图，",[45,448,240],{}," 可以，",[45,451,199],{},"\u002F",[45,454,352],{}," 不行，且与原数组共享数据。三者不要混用。",[136,457,459],{"id":458},"_8-并发场景下常用哪些阻塞队列怎么选","8. 并发场景下常用哪些阻塞队列？怎么选？",[11,461,462,465,466,469,470,473],{},[45,463,464],{},"ArrayBlockingQueue"," 有界、数组实现、单把锁配两个条件队列，适合容量可控的生产消费；",[45,467,468],{},"LinkedBlockingQueue"," 默认近似无界（",[45,471,472],{},"Integer.MAX_VALUE","），用两把锁分离入队出队，吞吐更高但必须显式设容量，否则堆积时容易 OOM。",[11,475,476,479,480,483,484,487,488,491],{},[45,477,478],{},"SynchronousQueue"," 不存储元素，生产者必须等到消费者接手，适合直接交接；",[45,481,482],{},"PriorityBlockingQueue"," 无界、按优先级出队；",[45,485,486],{},"DelayQueue"," 用于定时任务和缓存过期；",[45,489,490],{},"LinkedTransferQueue"," 在「生产者等待消费者」的场景下效率更高。",[11,493,494,495,498],{},"线程池选队列要结合拒绝策略一起看：用无界队列时 ",[45,496,497],{},"maximumPoolSize"," 基本不会生效，任务会一直排队，这是很多线上事故的根源。",[136,500,501],{"id":501},"易错点与自测",[503,504,505,525,543],"ul",{},[506,507,508,131,511,131,514,517,518,364,521,524],"li",{},[45,509,510],{},"subList",[45,512,513],{},"Arrays.asList",[45,515,516],{},"unmodifiableList"," 都是视图，只有 ",[45,519,520],{},"List.of",[45,522,523],{},"new ArrayList\u003C>(...)"," 是独立拷贝。",[506,526,527,528,530,531,534,535,538,539,542],{},"「线程安全」不等于「复合操作原子」：",[45,529,125],{}," 的 ",[45,532,533],{},"get"," 后再 ",[45,536,537],{},"put"," 仍需 ",[45,540,541],{},"computeIfAbsent"," 或显式锁。",[506,544,545,547,548,550,551,553,554,556],{},[45,546,79],{}," 允许一个 ",[45,549,174],{}," 键和多个 ",[45,552,174],{}," 值，",[45,555,125],{}," 全都不允许。",[11,558,559,560,562,563,565,566,568,569,571,572,574],{},"自测：① 说明为什么 ",[45,561,63],{}," 比 ",[45,564,53],{}," 更适合做队列；② 写出安全删除 ",[45,567,47],{}," 元素的三种方式；③ 解释 ",[45,570,520],{}," 与 ",[45,573,516],{}," 在数据同步上的差异。",[136,576,577],{"id":577},"参考资料",[503,579,580,590,601],{},[506,581,582,589],{},[583,584,588],"a",{"href":585,"rel":586},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F21\u002Fdocs\u002Fapi\u002Fjava.base\u002Fjava\u002Futil\u002Fpackage-summary.html",[587],"nofollow","java.util 包 API（Java 21）"," — 各集合实现的复杂度与线程安全声明",[506,591,592,597,598,600],{},[583,593,596],{"href":594,"rel":595},"https:\u002F\u002Fgithub.com\u002Fopenjdk\u002Fjdk\u002Fblob\u002Fmaster\u002Fsrc\u002Fjava.base\u002Fshare\u002Fclasses\u002Fjava\u002Futil\u002FArrayList.java",[587],"ArrayList 源码（OpenJDK）"," — 扩容与 ",[45,599,292],{}," 检查",[506,602,603,608,609,611],{},[583,604,607],{"href":605,"rel":606},"https:\u002F\u002Fopenjdk.org\u002Fjeps\u002F269",[587],"JEP 269：Convenience Factory Methods for Collections"," — ",[45,610,520],{}," 的不可变语义",{"title":613,"searchDepth":614,"depth":614,"links":615},"",2,[616,618,620,622,623,625,627,629,630,631],{"id":138,"depth":614,"text":617},"1. ArrayList、LinkedList、ArrayDeque 怎么选？",{"id":189,"depth":614,"text":619},"2. ArrayList 的扩容过程是怎样的？",{"id":244,"depth":614,"text":621},"3. HashMap、LinkedHashMap、TreeMap、ConcurrentHashMap 怎么选？",{"id":281,"depth":614,"text":282},{"id":327,"depth":614,"text":624},"5. CopyOnWriteArrayList 适合什么场景？代价是什么？",{"id":359,"depth":614,"text":626},"6. Comparable 和 Comparator 有什么区别？排序要注意什么？",{"id":413,"depth":614,"text":628},"7. List.of() 和 Collections.unmodifiableList() 有什么区别？",{"id":458,"depth":614,"text":459},{"id":501,"depth":614,"text":501},{"id":577,"depth":614,"text":577},"java-interview",null,"2026-10-24","对比 List、Map、Set 与阻塞队列的实现差异，讲清 fail-fast、CopyOnWrite 和不可变集合的适用边界","md","Java 核心",{},true,"20","\u002Fjava-interview\u002Fcollections",{"title":6,"description":635},{"loc":641},"java-interview\u002Fcollections","_f4iZ7Sm42r63hnX1xzkOGyJveWHm7tvdemmEZ4z0w4",{"id":647,"title":648,"body":649,"category":632,"cover":633,"date":634,"description":1098,"extension":636,"group":637,"meta":1099,"navigation":639,"order":1100,"path":1101,"seo":1102,"sitemap":1103,"stem":1104,"__hash__":1105},"javaInterview\u002Fjava-interview\u002Fconcurrency.md","Java 并发：JMM、锁与线程池",{"type":8,"value":650,"toc":1082},[651,658,722,726,751,754,776,782,795,798,801,807,814,820,826,830,841,844,855,863,877,890,902,906,912,915,918,929,936,949,957,975,979,994,1004,1007,1009,1036,1046,1048],[11,652,653,654,657],{},"并发题最忌讳只背名词。建议先建立一条主线：多个线程访问共享状态时，需要同时处理可见性、原子性和有序性，",[45,655,656],{},"volatile","、锁、CAS、线程池都是围绕这三件事展开的工具。",[15,659,660,673],{},[18,661,662],{},[21,663,664,667,670],{},[24,665,666],{},"工具",[24,668,669],{},"解决什么",[24,671,672],{},"不解决什么",[34,674,675,687,700,711],{},[21,676,677,681,684],{},[39,678,679],{},[45,680,656],{},[39,682,683],{},"可见性、禁止重排",[39,685,686],{},"复合操作原子性",[21,688,689,694,697],{},[39,690,691],{},[45,692,693],{},"synchronized",[39,695,696],{},"互斥、可见性、有序性",[39,698,699],{},"跨进程、超时中断",[21,701,702,705,708],{},[39,703,704],{},"CAS",[39,706,707],{},"单个变量的原子更新",[39,709,710],{},"多变量事务、ABA",[21,712,713,716,719],{},[39,714,715],{},"线程池",[39,717,718],{},"线程复用、限流、治理",[39,720,721],{},"任务内部的线程安全",[136,723,725],{"id":724},"_1-创建线程有哪几种方式为什么推荐线程池","1. 创建线程有哪几种方式？为什么推荐线程池？",[11,727,728,729,732,733,736,737,739,740,739,743,746,747,750],{},"本质上只有一种：构造 ",[45,730,731],{},"Thread"," 并调用 ",[45,734,735],{},"start()","。所谓多种方式是任务的定义形式不同：继承 ",[45,738,731],{},"、实现 ",[45,741,742],{},"Runnable",[45,744,745],{},"Callable"," 配合 ",[45,748,749],{},"FutureTask","、提交到线程池，JDK 21 还可以用虚拟线程执行器。",[11,752,753],{},"推荐线程池的原因：线程创建和销毁有系统调用与栈分配开销；无限制创建会耗尽内存和调度资源；池化后可以统一控制并发度、队列、拒绝策略、命名和监控。",[11,755,756,757,760,761,764,765,768,769,772,773,775],{},"直接用 ",[45,758,759],{},"new Thread"," 的代码通常缺少统一的异常处理和关闭逻辑。",[45,762,763],{},"execute"," 抛出的异常会交给线程的未捕获异常处理器，",[45,766,767],{},"submit"," 则会把它封装进 ",[45,770,771],{},"Future","，不调用 ",[45,774,533],{}," 就永远看不到——这是线上「任务静默失败」的常见原因。",[136,777,190,779,781],{"id":778},"_2-synchronized-的实现原理是什么锁升级是怎么回事",[45,780,693],{}," 的实现原理是什么？锁升级是怎么回事？",[11,783,784,786,787,790,791,794],{},[45,785,693],{}," 编译成 ",[45,788,789],{},"monitorenter"," \u002F ",[45,792,793],{},"monitorexit"," 字节码，修饰方法时通过方法访问标志隐式加锁。锁信息记录在对象头的 Mark Word 中。",[11,796,797],{},"传统讲解分为偏向锁、轻量级锁、重量级锁：无竞争时偏向某个线程；出现竞争时用 CAS 尝试把 Mark Word 指向栈上的锁记录；竞争加剧则膨胀为操作系统互斥量，未抢到的线程进入等待队列。",[11,799,800],{},"需要补充版本边界：偏向锁从 JDK 15 起默认关闭并被标记废弃，后续版本已移除，现代 JDK 不应再把它当作必然存在的优化。锁升级在常规理解中不可逆，但具体实现有安全点等细节，面试不必展开。",[136,802,245,804,806],{"id":803},"_3-volatile-能保证什么不能保证什么",[45,805,656],{}," 能保证什么、不能保证什么？",[11,808,809,810,813],{},"它保证可见性：写操作会刷新到主内存，读操作会从主内存重新读取；同时禁止特定重排序，建立 happens-before 关系。它不保证复合操作的原子性，",[45,811,812],{},"count++"," 依然是读、加、写三步。",[11,815,816,817,819],{},"典型正确用法：状态标志位、一次性安全发布（双重检查锁单例的实例字段）、作为 CAS 的底层语义基础。典型错误用法：用 ",[45,818,656],{}," 修饰计数器，或用它保护「先检查再修改」的逻辑。",[11,821,822,823,825],{},"双重检查锁里必须给实例字段加 ",[45,824,656],{},"，否则可能先写入引用、后完成构造，其他线程会看到一个「半初始化」对象。",[136,827,829],{"id":828},"_4-cas-是什么aba-问题怎么解决","4. CAS 是什么？ABA 问题怎么解决？",[11,831,832,833,836,837,840],{},"CAS 包含三个值：内存位置、期望值、新值，仅当当前值等于期望值时才更新，由 CPU 指令保证原子性。Java 通过 ",[45,834,835],{},"Unsafe"," 的本地方法暴露，",[45,838,839],{},"AtomicInteger"," 等类封装了它。",[11,842,843],{},"它避免了加锁的阻塞和上下文切换，但有几个问题：自旋失败会空耗 CPU；只能保证单个变量的原子性；存在 ABA——值从 A 改成 B 又改回 A，CAS 无法察觉中间变化。",[11,845,846,847,850,851,854],{},"ABA 的解决思路是加版本号，",[45,848,849],{},"AtomicStampedReference"," 就是「值 + 版本」；如果对象引用变化本身无害，也可以忽略。高并发计数场景可以用 ",[45,852,853],{},"LongAdder","，它把热点分散到多个 Cell，代价是求和结果不是某一瞬间的精确快照。",[136,856,858,859,364,861,146],{"id":857},"_5-aqs-的原理是什么reentrantlock-和-synchronized-怎么选","5. AQS 的原理是什么？",[45,860,337],{},[45,862,693],{},[11,864,865,866,869,870,790,873,876],{},"AQS 用一个 ",[45,867,868],{},"volatile int state"," 表示同步状态，加一个 FIFO 的双向等待队列。获取失败的线程被包装成节点入队并挂起，释放时唤醒后继节点；子类只需实现 ",[45,871,872],{},"tryAcquire",[45,874,875],{},"tryRelease"," 等模板方法。",[11,878,879,881,882,885,886,889],{},[45,880,337],{}," 用 state 记录重入次数，",[45,883,884],{},"Semaphore"," 用它表示许可数，",[45,887,888],{},"CountDownLatch"," 用它表示剩余计数。公平锁在获取时先检查队列中是否有前驱，非公平锁允许新线程直接抢，吞吐通常更高。",[11,891,892,893,895,896,898,899,901],{},"选型上：需要可中断获取、超时、公平性或多个条件队列时用 ",[45,894,337],{},"；否则优先 ",[45,897,693],{},"，它由 JVM 优化、写法简单、不会忘记释放。虚拟线程场景下还要注意，JDK 21 的 ",[45,900,693],{}," 会钉住载体线程。",[136,903,905],{"id":904},"_6-线程池的核心参数和执行流程线程数怎么设","6. 线程池的核心参数和执行流程？线程数怎么设？",[11,907,908,911],{},[45,909,910],{},"ThreadPoolExecutor"," 有七个参数：核心线程数、最大线程数、空闲存活时间、时间单位、工作队列、线程工厂、拒绝策略。",[11,913,914],{},"提交任务时：核心线程未满就新建线程；核心满了进队列；队列满了且未达最大线程数就继续建线程；再满则执行拒绝策略。所以无界队列会让最大线程数永远不生效。",[11,916,917],{},"线程数没有万能公式。CPU 密集任务通常取核数或核数加一；IO 密集可用「核数 ×（1 + 等待时间 \u002F 计算时间）」估算，再结合实际压测。比公式更重要的是给队列设上界、给线程命名、监控活跃数与队列长度。",[11,919,920,921,924,925,928],{},"拒绝策略默认是 ",[45,922,923],{},"AbortPolicy","，直接抛异常。 ",[45,926,927],{},"CallerRunsPolicy"," 让提交线程执行，能形成天然反压，但会阻塞上游，需要确认业务能接受。",[136,930,414,932,935],{"id":931},"_7-threadlocal-的原理和内存泄漏原因",[45,933,934],{},"ThreadLocal"," 的原理和内存泄漏原因？",[11,937,938,939,941,942,945,946,948],{},"每个 ",[45,940,731],{}," 内部有一个 ",[45,943,944],{},"ThreadLocalMap","，key 是 ",[45,947,934],{}," 的弱引用，value 是强引用。线程复用（尤其是线程池）时，map 会跟着线程长期存活。",[11,950,951,952,131,954,956],{},"泄漏的根源不是 key 的弱引用本身，而是「key 被回收后，value 仍被 Entry 强引用，而 Entry 又被线程持有」。虽然 ",[45,953,533],{},[45,955,240],{}," 会顺带清理部分过期条目，但不保证及时。",[11,958,959,960,963,964,300,967,970,971,974],{},"规范做法是在 ",[45,961,962],{},"finally"," 中调用 ",[45,965,966],{},"remove()",[45,968,969],{},"InheritableThreadLocal"," 只在创建线程时拷贝，线程池复用会失效；跨线程池传递上下文需要 ",[45,972,973],{},"TransmittableThreadLocal"," 这类方案。虚拟线程数量极大，更要注意 ThreadLocal 的内存占用。",[136,976,978],{"id":977},"_8-如何排查死锁虚拟线程又解决了什么问题","8. 如何排查死锁？虚拟线程又解决了什么问题？",[11,980,981,982,985,986,989,990,993],{},"死锁需要同时满足互斥、持有并等待、不可抢占、循环等待四个条件。排查时先用 ",[45,983,984],{},"jcmd \u003Cpid> Thread.print -l"," 或 ",[45,987,988],{},"jstack"," 抓线程栈，JDK 会直接输出 ",[45,991,992],{},"Found one Java-level deadlock"," 以及相互等待的锁和代码行。",[11,995,996,997,1000,1001,300],{},"预防手段：统一加锁顺序、用 ",[45,998,999],{},"tryLock"," 带超时、缩小临界区、避免在持锁时调用外部服务。数据库死锁则是另一套机制，要看 ",[45,1002,1003],{},"SHOW ENGINE INNODB STATUS",[11,1005,1006],{},"虚拟线程解决的是「大量阻塞任务导致平台线程耗尽」的问题：阻塞时自动让出载体线程，调度成本低，适合高并发 IO。它不提升单核计算能力，也不能突破下游连接池、数据库连接数的瓶颈；用信号量限制并发往往比池化虚拟线程更合适。",[136,1008,501],{"id":501},[503,1010,1011,1022,1030],{},[506,1012,1013,1015,1016,1018,1019,1021],{},[45,1014,656],{}," 不等于原子，",[45,1017,693],{}," 不等于公平，",[45,1020,125],{}," 不等于复合操作原子。",[506,1023,1024,1027,1028,300],{},[45,1025,1026],{},"Executors"," 的快捷工厂方法隐藏了无界队列和无上限线程数，生产环境应显式构造 ",[45,1029,910],{},[506,1031,1032,1033,1035],{},"ThreadLocal 必须 ",[45,1034,352],{},"，线程池与虚拟线程会放大忘记清理的后果。",[11,1037,1038,1039,1041,1042,1045],{},"自测：① 写出双重检查锁单例并解释 ",[45,1040,656],{}," 的作用；② 说明线程池在「核心满、队列满、未到最大线程数」时的行为；③ 用 ",[45,1043,1044],{},"jcmd"," 命令列出排查死锁的操作。",[136,1047,577],{"id":577},[503,1049,1050,1058,1066,1074],{},[506,1051,1052,1057],{},[583,1053,1056],{"href":1054,"rel":1055},"https:\u002F\u002Fdocs.oracle.com\u002Fjavase\u002Fspecs\u002Fjls\u002Fse21\u002Fhtml\u002Fjls-17.html",[587],"JLS SE 21 第 17 章：线程与锁"," — happens-before 与内存模型的规范定义",[506,1059,1060,1065],{},[583,1061,1064],{"href":1062,"rel":1063},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F21\u002Fdocs\u002Fapi\u002Fjava.base\u002Fjava\u002Futil\u002Fconcurrent\u002FThreadPoolExecutor.html",[587],"ThreadPoolExecutor API"," — 参数语义、执行流程与拒绝策略",[506,1067,1068,1073],{},[583,1069,1072],{"href":1070,"rel":1071},"https:\u002F\u002Fopenjdk.org\u002Fjeps\u002F374",[587],"JEP 374：Deprecate and Disable Biased Locking"," — 偏向锁的版本边界",[506,1075,1076,1081],{},[583,1077,1080],{"href":1078,"rel":1079},"https:\u002F\u002Fopenjdk.org\u002Fjeps\u002F444",[587],"JEP 444：Virtual Threads"," — 虚拟线程的调度模型与限制",{"title":613,"searchDepth":614,"depth":614,"links":1083},[1084,1085,1087,1089,1090,1092,1093,1095,1096,1097],{"id":724,"depth":614,"text":725},{"id":778,"depth":614,"text":1086},"2. synchronized 的实现原理是什么？锁升级是怎么回事？",{"id":803,"depth":614,"text":1088},"3. volatile 能保证什么、不能保证什么？",{"id":828,"depth":614,"text":829},{"id":857,"depth":614,"text":1091},"5. AQS 的原理是什么？ReentrantLock 和 synchronized 怎么选？",{"id":904,"depth":614,"text":905},{"id":931,"depth":614,"text":1094},"7. ThreadLocal 的原理和内存泄漏原因？",{"id":977,"depth":614,"text":978},{"id":501,"depth":614,"text":501},{"id":577,"depth":614,"text":577},"围绕可见性、原子性、有序性梳理 volatile、CAS、AQS、线程池和 ThreadLocal，补上虚拟线程的适用边界",{},"40","\u002Fjava-interview\u002Fconcurrency",{"title":648,"description":1098},{"loc":1101},"java-interview\u002Fconcurrency","YRV5UW4RO-4T4W3r6nZwRHvUi2h4iXBgYFvE3TUoWyI",{"id":1107,"title":1108,"body":1109,"category":632,"cover":633,"date":634,"description":1471,"extension":636,"group":1472,"meta":1473,"navigation":639,"order":1474,"path":1475,"seo":1476,"sitemap":1477,"stem":1478,"__hash__":1479},"javaInterview\u002Fjava-interview\u002Fdesign-patterns.md","设计模式：原则、单例与 Spring 中的应用",{"type":8,"value":1110,"toc":1459},[1111,1114,1118,1121,1124,1127,1130,1133,1137,1140,1146,1149,1152,1156,1163,1170,1176,1183,1187,1194,1287,1294,1297,1301,1315,1324,1327,1330,1334,1337,1340,1350,1357,1360,1364,1375,1386,1389,1393,1396,1399,1402,1405,1407,1418,1427,1429,1455],[11,1112,1113],{},"设计模式题最怕两种回答：一种是背定义，另一种是把所有模式都往项目里塞。好的回答会说明「这个变化点在哪里、引入模式后谁不需要改」。",[136,1115,1117],{"id":1116},"_1-solid-原则怎么落到代码里","1. SOLID 原则怎么落到代码里？",[11,1119,1120],{},"单一职责：一个类只对一类变化负责。判断标准不是「代码行数」，而是「改一个需求时，是否总在改同一个类」。",[11,1122,1123],{},"开闭原则：对扩展开放、对修改关闭。实践上靠接口、策略注册和依赖注入，而不是靠预测所有未来需求。",[11,1125,1126],{},"里氏替换：子类必须能在不破坏语义的前提下替换父类。典型反例是正方形继承长方形、或子类重写方法后抛出父类没有的异常。",[11,1128,1129],{},"接口隔离：接口要小而专，避免实现类被迫实现用不到的方法。依赖倒置：高层模块依赖抽象，Spring 的注入就是它的落地。",[11,1131,1132],{},"此外还有迪米特法则（少暴露内部结构）、组合优于继承。原则之间会冲突，真正的能力是在具体场景里取舍。",[136,1134,1136],{"id":1135},"_2-单例有几种写法哪种最好","2. 单例有几种写法？哪种最好？",[11,1138,1139],{},"饿汉式在类加载时就创建，简单且线程安全，但会提前占用资源；懒汉式延迟创建，朴素的加锁实现性能差。",[11,1141,1142,1143,1145],{},"双重检查锁需要 ",[45,1144,656],{}," 修饰实例，否则可能读到「引用已赋值但构造未完成」的对象。静态内部类利用类加载机制实现懒加载和线程安全，写法最简洁。",[11,1147,1148],{},"枚举单例天然防止反射和反序列化破坏，是《Effective Java》推荐的方式；如果不需要延迟加载，它是最稳的选择。",[11,1150,1151],{},"要注意：Spring 的单例是「容器内单例」，不是 JVM 全局单例；一个类可以被多个容器各创建一个实例。分布式场景下，单例也只在本进程内有效。",[136,1153,1155],{"id":1154},"_3-工厂模式解决了什么问题","3. 工厂模式解决了什么问题？",[11,1157,1158,1159,1162],{},"简单工厂用一个方法按参数创建对象，把 ",[45,1160,1161],{},"new"," 集中起来；工厂方法把创建延迟到子类；抽象工厂用于创建一组相关产品。",[11,1164,1165,1166,1169],{},"它们的共同价值是：调用方只依赖抽象，不关心具体类型和构造细节；新增产品时尽量不改调用方。Spring 的 ",[45,1167,1168],{},"BeanFactory"," 就是工厂思想，配合配置和依赖注入，把对象创建与使用解耦。",[11,1171,1172,1173,1175],{},"但工厂不是越多越好。对象只有一个实现、构造也简单时，直接 ",[45,1174,1161],{}," 更清楚。工厂真正有价值，是在创建逻辑复杂（需要选择实现、组装依赖、缓存实例）或需要统一生命周期时。",[11,1177,1178,1179,1182],{},"如果工厂里出现大量 ",[45,1180,1181],{},"if\u002Felse"," 分支，往往说明可以用注册表加策略来替代。",[136,1184,1186],{"id":1185},"_4-策略模式怎么消除-if-else边界在哪","4. 策略模式怎么消除 if-else？边界在哪？",[11,1188,1189,1190,1193],{},"先抽象出策略接口，每个分支实现一个策略；再用 ",[45,1191,1192],{},"Map\u003CString, Strategy>"," 注册，调用时按类型取策略。注册可以手写，也可以让 Spring 注入所有实现并按标记建索引。",[1195,1196,1200],"pre",{"className":1197,"code":1198,"language":1199,"meta":613,"style":613},"language-java shiki shiki-themes github-dark","\u002F**\n * 按支付渠道选择策略并执行支付。\n *\n * @param channel 支付渠道标识\n * @param amount  支付金额，单位分\n * @return 支付结果\n *\u002F\npublic PayResult pay(String channel, long amount) {\n    PayStrategy strategy = strategies.get(channel);\n    if (strategy == null) {\n        throw new IllegalArgumentException(\"不支持的渠道: \" + channel);\n    }\n    return strategy.pay(amount);\n}\n","java",[45,1201,1202,1210,1215,1221,1227,1233,1239,1245,1251,1257,1263,1269,1275,1281],{"__ignoreMap":613},[1203,1204,1207],"span",{"class":1205,"line":1206},"line",1,[1203,1208,1209],{},"\u002F**\n",[1203,1211,1212],{"class":1205,"line":614},[1203,1213,1214],{}," * 按支付渠道选择策略并执行支付。\n",[1203,1216,1218],{"class":1205,"line":1217},3,[1203,1219,1220],{}," *\n",[1203,1222,1224],{"class":1205,"line":1223},4,[1203,1225,1226],{}," * @param channel 支付渠道标识\n",[1203,1228,1230],{"class":1205,"line":1229},5,[1203,1231,1232],{}," * @param amount  支付金额，单位分\n",[1203,1234,1236],{"class":1205,"line":1235},6,[1203,1237,1238],{}," * @return 支付结果\n",[1203,1240,1242],{"class":1205,"line":1241},7,[1203,1243,1244],{}," *\u002F\n",[1203,1246,1248],{"class":1205,"line":1247},8,[1203,1249,1250],{},"public PayResult pay(String channel, long amount) {\n",[1203,1252,1254],{"class":1205,"line":1253},9,[1203,1255,1256],{},"    PayStrategy strategy = strategies.get(channel);\n",[1203,1258,1260],{"class":1205,"line":1259},10,[1203,1261,1262],{},"    if (strategy == null) {\n",[1203,1264,1266],{"class":1205,"line":1265},11,[1203,1267,1268],{},"        throw new IllegalArgumentException(\"不支持的渠道: \" + channel);\n",[1203,1270,1272],{"class":1205,"line":1271},12,[1203,1273,1274],{},"    }\n",[1203,1276,1278],{"class":1205,"line":1277},13,[1203,1279,1280],{},"    return strategy.pay(amount);\n",[1203,1282,1284],{"class":1205,"line":1283},14,[1203,1285,1286],{},"}\n",[11,1288,1289,1290,1293],{},"边界在于：如果分支逻辑很短、变化不频繁，保留 ",[45,1291,1292],{},"switch"," 反而更易读。策略模式增加的是类数量和间接层，只有当每个分支都有独立复杂度、需要独立测试或扩展时才划算。",[11,1295,1296],{},"还要避免「策略只包一层空壳」：如果所有实现都在调同一个方法，只是参数不同，可能用配置更合适。",[136,1298,1300],{"id":1299},"_5-模板方法和回调有什么区别","5. 模板方法和回调有什么区别？",[11,1302,1303,1304,131,1307,1310,1311,1314],{},"模板方法在父类定义算法骨架，把可变步骤留给子类实现，属于继承式扩展。",[45,1305,1306],{},"JdbcTemplate",[45,1308,1309],{},"AbstractList","、Servlet 的 ",[45,1312,1313],{},"service"," 都是这个思路。",[11,1316,1317,1318,530,1321,300],{},"回调把一段行为作为参数传入，由框架在合适时机调用，属于组合式扩展。Java 里用函数式接口、匿名类或 Lambda 表达，比如 ",[45,1319,1320],{},"JdbcTemplate.query",[45,1322,1323],{},"RowMapper",[11,1325,1326],{},"区别在耦合方式：模板方法要求继承，父类改动会影响所有子类；回调更灵活，可以复用同一个方法，也便于测试。",[11,1328,1329],{},"选择时优先组合：如果变化点只有一个或少数几个，回调足够；如果骨架固定且步骤较多，模板方法更清晰。Spring 的很多「模板」类其实同时提供了回调和子类扩展两种方式。",[136,1331,1333],{"id":1332},"_6-代理装饰适配器怎么区分","6. 代理、装饰、适配器怎么区分？",[11,1335,1336],{},"三者都通过包装对象改变行为，但目的不同。",[11,1338,1339],{},"代理控制访问：调用方和被代理对象实现同一接口，代理在调用前后加逻辑（鉴权、事务、远程调用）。Spring AOP 就是代理。",[11,1341,1342,1343,1346,1347,300],{},"装饰增强功能：同样实现同一接口，但重点是叠加职责，可以层层嵌套，比如 ",[45,1344,1345],{},"BufferedInputStream"," 包 ",[45,1348,1349],{},"FileInputStream",[11,1351,1352,1353,1356],{},"适配器转换接口：把一个接口转成调用方期望的另一个接口，解决不兼容问题，比如 ",[45,1354,1355],{},"InputStreamReader"," 把字节流转成字符流。",[11,1358,1359],{},"判断方法：看包装后接口是否变化、调用方是否需要感知。接口不变且为了控制访问是代理，为了增强是装饰，接口变了是适配器。",[136,1361,1363],{"id":1362},"_7-观察者模式在-spring-里怎么用","7. 观察者模式在 Spring 里怎么用？",[11,1365,1366,1367,1370,1371,1374],{},"Spring 的事件机制就是观察者模式：",[45,1368,1369],{},"ApplicationEventPublisher"," 发布事件，",[45,1372,1373],{},"@EventListener"," 标注的方法接收事件，容器负责找到监听器。",[11,1376,1377,1378,1381,1382,1385],{},"默认是同步调用，发布者会等所有监听器执行完；可以用 ",[45,1379,1380],{},"@Async"," 改异步，但要配置线程池，并注意异常不会传回发布者。",[45,1383,1384],{},"@TransactionalEventListener"," 可以把监听时机绑定到事务提交后，避免「事务还没提交，监听器已经查库」。",[11,1387,1388],{},"风险包括：监听器抛异常影响主流程（同步时）、异步丢失顺序、循环发布导致死循环、监听器过多难以追踪。事件适合「一对多、弱依赖」的通知，不适合需要强一致返回结果的流程。",[136,1390,1392],{"id":1391},"_8-责任链模式适合什么场景","8. 责任链模式适合什么场景？",[11,1394,1395],{},"责任链把请求沿处理器链传递，每个处理器决定处理、继续还是中断。Servlet 的 Filter、Spring MVC 的 Interceptor、Netty 的 Pipeline 都是它的实现。",[11,1397,1398],{},"它的价值是解耦：新增一个校验或日志处理器，不需要改调用方；顺序和组合可以通过配置调整。",[11,1400,1401],{},"代价是链路不直观，出问题时需要知道「谁处理了、谁短路了」。要明确每个处理器的职责和顺序，并提供日志或追踪。",[11,1403,1404],{},"不要为了「看起来灵活」把简单流程拆成一堆处理器。两三个步骤的校验，用顺序代码更清楚；只有当处理器数量会增长、顺序需要动态调整时，责任链才值得。",[136,1406,501],{"id":501},[503,1408,1409,1412,1415],{},[506,1410,1411],{},"设计原则是权衡工具，不是越抽象越好；模式引入的间接层要有明确收益。",[506,1413,1414],{},"单例的线程安全取决于是否有可变状态，容器单例不等于全局单例。",[506,1416,1417],{},"Spring 事件默认同步，异常处理和事务边界要显式设计。",[11,1419,1420,1421,1423,1424,1426],{},"自测：① 用策略模式改写一段三层的 ",[45,1422,1181],{},"，并说明什么时候不该改；② 区分代理和装饰的意图差异；③ 解释 ",[45,1425,1384],{}," 解决什么问题。",[136,1428,577],{"id":577},[503,1430,1431,1439,1447],{},[506,1432,1433,1438],{},[583,1434,1437],{"href":1435,"rel":1436},"https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Freference\u002Fcore\u002Fbeans\u002Fcontext-introduction.html#context-functionality-events",[587],"Spring Framework：事件与监听器"," — 同步、异步与事务事件",[506,1440,1441,1446],{},[583,1442,1445],{"href":1443,"rel":1444},"https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Freference\u002Fcore\u002Faop.html",[587],"Spring Framework：AOP 与代理"," — 代理模式的实际应用",[506,1448,1449,1454],{},[583,1450,1453],{"href":1451,"rel":1452},"https:\u002F\u002Frefactoring.guru\u002Fdesign-patterns",[587],"Refactoring.Guru：设计模式目录"," — 各模式的意图与结构对照",[1456,1457,1458],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":613,"searchDepth":614,"depth":614,"links":1460},[1461,1462,1463,1464,1465,1466,1467,1468,1469,1470],{"id":1116,"depth":614,"text":1117},{"id":1135,"depth":614,"text":1136},{"id":1154,"depth":614,"text":1155},{"id":1185,"depth":614,"text":1186},{"id":1299,"depth":614,"text":1300},{"id":1332,"depth":614,"text":1333},{"id":1362,"depth":614,"text":1363},{"id":1391,"depth":614,"text":1392},{"id":501,"depth":614,"text":501},{"id":577,"depth":614,"text":577},"用 SOLID 原则串起单例、工厂、策略、模板方法、代理和责任链，说明它们在 Spring 中的真实落点与过度设计边界","分布式与工程",{},"150","\u002Fjava-interview\u002Fdesign-patterns",{"title":1108,"description":1471},{"loc":1475},"java-interview\u002Fdesign-patterns","LDn2dRYU8whvxaYpRM9NbjeMuySiGmllY-cAe305Qd4",{"id":1481,"title":1482,"body":1483,"category":632,"cover":633,"date":634,"description":1696,"extension":636,"group":1472,"meta":1697,"navigation":639,"order":1698,"path":1699,"seo":1700,"sitemap":1701,"stem":1702,"__hash__":1703},"javaInterview\u002Fjava-interview\u002Fdistributed-systems.md","分布式系统：CAP、ID 与分库分表",{"type":8,"value":1484,"toc":1684},[1485,1488,1492,1495,1498,1501,1504,1508,1511,1514,1517,1520,1524,1527,1534,1537,1540,1544,1551,1554,1561,1564,1568,1571,1574,1577,1580,1584,1591,1594,1597,1600,1604,1607,1610,1613,1616,1620,1623,1626,1629,1632,1634,1645,1648,1650],[11,1486,1487],{},"分布式题很容易变成名词接龙。建议抓住一条判断标准：在故障和并发下，系统愿意牺牲什么、又能保证到什么程度。下面每个问题都尽量落到这个取舍上。",[136,1489,1491],{"id":1490},"_1-cap-和-base-到底在说什么","1. CAP 和 BASE 到底在说什么？",[11,1493,1494],{},"C 是一致性，通常指线性一致：读到的必须是最新已完成的写；A 是可用性，每个请求都能在有限时间内得到非错误响应；P 是分区容错，网络分区时系统仍能继续工作。",[11,1496,1497],{},"关键点是：网络分区不是「要不要容忍」，而是迟早会发生。所以真实选择是「分区发生时保 C 还是保 A」，而不是平时三选二。很多回答说「我们选了 CA」，在跨机房部署下基本不成立。",[11,1499,1500],{},"BASE 是对 AP 的工程化补充：基本可用、软状态、最终一致。它不是放弃正确性，而是把一致性从「时刻」放宽到「一段时间内」。",[11,1502,1503],{},"还有一个常被追问的 PACELC：分区时在 C 和 A 之间选，没有分区时在延迟和一致性之间选。多数系统平时也要为一致性付出延迟。",[136,1505,1507],{"id":1506},"_2-分布式-id-有哪些方案雪花算法的坑在哪","2. 分布式 ID 有哪些方案？雪花算法的坑在哪？",[11,1509,1510],{},"UUID 本地生成、无依赖，但无序且较长，做主键会造成 B+ 树页分裂和索引膨胀。数据库自增简单可靠，但单点、扩展难，分库后需要步长或号段。",[11,1512,1513],{},"雪花算法生成 64 位 ID：1 位符号、41 位毫秒时间、10 位机器标识、12 位序列。趋势递增、性能高，但强依赖时钟，回拨可能产生重复 ID；机器标识的分配也需要统一管理。",[11,1515,1516],{},"常见的回拨处理：小幅回拨等待追平，大幅回拨直接报错或切换备用 workerId；也可以用「上次时间戳 + 序列」的本地状态兜底。",[11,1518,1519],{},"号段模式（如 Leaf）从数据库批量取一段 ID 在内存分配，减少数据库压力，但要处理号段耗尽时的预取和容灾。最终选型看是否需要趋势递增、能否接受中心依赖，以及前端是否会被 JS 精度截断。",[136,1521,1523],{"id":1522},"_3-如何设计一个幂等接口","3. 如何设计一个幂等接口？",[11,1525,1526],{},"幂等的本质是「同一个业务意图执行多次，结果与执行一次相同」。先确定幂等键：客户端生成的请求号、业务单号，或「用户 + 操作 + 目标」的组合。",[11,1528,1529,1530,1533],{},"常见实现：数据库唯一索引（最可靠，但要注意捕获重复键异常）；去重表或 Redis ",[45,1531,1532],{},"SET NX"," 记录已处理键（要设置合理 TTL）；状态机限制非法迁移；乐观锁版本号；Token 机制（先领 token，提交时消费）。",[11,1535,1536],{},"还要处理并发窗口：两个相同请求同时到达，都查到「未处理」，然后都执行。唯一约束或分布式锁能把窗口关掉，单纯「先查再写」不行。",[11,1538,1539],{},"设计时明确三点：幂等键由谁生成、保存多久、重复请求返回什么（成功结果、进行中还是冲突）。这些约定比具体技术更重要。",[136,1541,1543],{"id":1542},"_4-分布式锁有哪些实现怎么选","4. 分布式锁有哪些实现？怎么选？",[11,1545,1546,1547,1550],{},"Redis 锁性能高、实现简单，",[45,1548,1549],{},"SET NX PX"," 加 Lua 释放，配合续期；但主从异步复制下存在锁丢失窗口，适合防重复而非强互斥。",[11,1552,1553],{},"ZooKeeper 用临时顺序节点：创建最小序号节点者获得锁，其他节点监听前一个节点。会话断开后临时节点自动删除，可靠性较好；缺点是性能低于 Redis，且依赖 ZK 集群。",[11,1555,1556,1557,1560],{},"数据库锁可以用唯一索引或 ",[45,1558,1559],{},"SELECT ... FOR UPDATE","，实现最简单，但性能和扩展性最差，适合低频、强一致的场景。",[11,1562,1563],{},"选择时看失败代价：重复发一条通知，Redis 锁足够；扣减余额，应该用数据库行锁、乐观锁或共识系统，并配合 fencing token 防止旧持有者继续写。",[136,1565,1567],{"id":1566},"_5-为什么要分库分表会带来哪些问题","5. 为什么要分库分表？会带来哪些问题？",[11,1569,1570],{},"单表数据量大时，索引变深、DDL 变慢、备份恢复困难；单库连接数和写入能力也有上限。分表解决数据量，分库解决连接数和写扩展。",[11,1572,1573],{},"拆分方式有垂直和水平：垂直按业务或字段拆，边界清晰但可能留下大表；水平按分片键拆，扩展性好，但跨片查询变难。",[11,1575,1576],{},"问题清单要能说出来：跨库 join、跨片分页和聚合、分布式事务、全局唯一 ID、跨片唯一约束、路由规则变更、数据迁移与扩容、热点分片。",[11,1578,1579],{},"工程上通常配合中间件（如 ShardingSphere）做路由和归并，但中间件不能消灭跨片成本。设计分片键时优先选查询必带、分布均匀的字段；如果业务必须按两个维度查，考虑冗余表或搜索引擎。",[136,1581,1583],{"id":1582},"_6-一致性哈希解决了什么问题","6. 一致性哈希解决了什么问题？",[11,1585,1586,1587,1590],{},"普通取模 ",[45,1588,1589],{},"hash(key) % N"," 在节点数变化时几乎全部 key 都要重新映射，缓存会大面积失效。一致性哈希把节点和 key 映射到同一个环上，key 顺时针找到第一个节点，增删节点只影响相邻区间。",[11,1592,1593],{},"为了解决节点分布不均，引入了虚拟节点：每个物理节点在环上放置多个副本，key 的分布更均匀，节点扩容时迁移也更平滑。",[11,1595,1596],{},"它仍有两个边界：一是热点 key 不会因为哈希而分散，需要额外拆分；二是节点性能不同，等权虚拟节点可能造成负载不均，要按权重调整。",[11,1598,1599],{},"一致性哈希常用于缓存和对象存储，数据库分片更常用范围或哈希取模，因为要兼顾范围查询和运维可控性。",[136,1601,1603],{"id":1602},"_7-zookeeper-的典型用法和原理","7. ZooKeeper 的典型用法和原理？",[11,1605,1606],{},"ZooKeeper 是一个高可用的协调服务，数据组织成树形 znode，支持持久节点、临时节点和顺序节点。它通过 ZAB 协议在 leader 和 follower 之间复制日志，写请求由 leader 处理，多数派确认后提交。",[11,1608,1609],{},"临时节点随会话消失，因此常用于服务注册和故障感知；顺序节点配合 watch 可以实现公平锁；节点数据小、读多写少，适合存元数据和配置，不适合存大对象或高频写。",[11,1611,1612],{},"watch 是一次性的：触发后需要重新注册，客户端要处理「注册前发生变化」的窗口。会话超时时间决定了故障感知的快慢，设得太短容易误判，太长则恢复慢。",[11,1614,1615],{},"同类系统 etcd 基于 Raft，提供租约、MVCC 和更现代的 API；选型时看生态和团队经验，不要只看「谁更快」。",[136,1617,1619],{"id":1618},"_8-系统高可用要做哪些事","8. 系统高可用要做哪些事？",[11,1621,1622],{},"先定义目标：RTO（多久恢复）和 RPO（能丢多少数据），不同业务差异很大。然后从冗余、故障转移、隔离和演练四方面设计。",[11,1624,1625],{},"冗余包括多实例、多可用区、多副本；故障转移要有健康检查、选主和流量摘除，并防止脑裂——常用多数派、租约和 fencing 机制。隔离包括线程池、连接池、机房和租户隔离，避免一个依赖拖垮全局。",[11,1627,1628],{},"降级和限流是「保命」手段：非核心功能可关闭，核心链路保留最小依赖。所有降级开关要提前演练，不能等到故障时才发现配置不生效。",[11,1630,1631],{},"最后是演练和演练后的修复：混沌工程、故障注入、备份恢复演练。没有演练过的高可用方案，通常只是文档上的高可用。",[136,1633,501],{"id":501},[503,1635,1636,1639,1642],{},[506,1637,1638],{},"CAP 不是「三选二」，分区发生时才是真正的取舍点；BASE 是最终一致，不是「不一致也行」。",[506,1640,1641],{},"雪花 ID 的趋势递增依赖时钟，回拨处理必须明确。",[506,1643,1644],{},"分库分表能解决容量，但会把查询和事务复杂度推到应用层。",[11,1646,1647],{},"自测：① 解释为什么 UUID 不适合做 InnoDB 主键；② 说明一致性哈希中虚拟节点解决什么问题；③ 给出一个「并发重复请求」的幂等落地方案。",[136,1649,577],{"id":577},[503,1651,1652,1660,1668,1676],{},[506,1653,1654,1659],{},[583,1655,1658],{"href":1656,"rel":1657},"https:\u002F\u002Fzookeeper.apache.org\u002Fdoc\u002Fcurrent\u002FzookeeperProgrammers.html",[587],"ZooKeeper 官方文档：程序员指南"," — znode、watch 与会话语义",[506,1661,1662,1667],{},[583,1663,1666],{"href":1664,"rel":1665},"https:\u002F\u002Fshardingsphere.apache.org\u002Fdocument\u002Fcurrent\u002Fen\u002Foverview\u002F",[587],"Apache ShardingSphere 文档"," — 分片、路由与分布式事务",[506,1669,1670,1675],{},[583,1671,1674],{"href":1672,"rel":1673},"https:\u002F\u002Fetcd.io\u002Fdocs\u002F",[587],"etcd 官方文档"," — Raft、租约与 watch 的对照参考",[506,1677,1678,1683],{},[583,1679,1682],{"href":1680,"rel":1681},"https:\u002F\u002Fsre.google\u002Fsre-book\u002Fhandling-overload\u002F",[587],"Google SRE Book：处理过载"," — 限流、降级与容量设计",{"title":613,"searchDepth":614,"depth":614,"links":1685},[1686,1687,1688,1689,1690,1691,1692,1693,1694,1695],{"id":1490,"depth":614,"text":1491},{"id":1506,"depth":614,"text":1507},{"id":1522,"depth":614,"text":1523},{"id":1542,"depth":614,"text":1543},{"id":1566,"depth":614,"text":1567},{"id":1582,"depth":614,"text":1583},{"id":1602,"depth":614,"text":1603},{"id":1618,"depth":614,"text":1619},{"id":501,"depth":614,"text":501},{"id":577,"depth":614,"text":577},"讲清 CAP\u002FBASE 的真实含义、分布式 ID 与幂等设计，并梳理分库分表、一致性哈希和高可用的工程权衡",{},"120","\u002Fjava-interview\u002Fdistributed-systems",{"title":1482,"description":1696},{"loc":1699},"java-interview\u002Fdistributed-systems","lFowmIzCC56o9oMSB8MmUA3hDHOXU_nkU9zxXjUyHnA",{"id":1705,"title":1706,"body":1707,"category":632,"cover":633,"date":634,"description":2040,"extension":636,"group":637,"meta":2041,"navigation":639,"order":2042,"path":2043,"seo":2044,"sitemap":2045,"stem":2046,"__hash__":2047},"javaInterview\u002Fjava-interview\u002Fhashmap.md","HashMap 的底层结构、扩容与线程安全",{"type":8,"value":1708,"toc":2027},[1709,1712,1716,1719,1726,1737,1740,1744,1759,1766,1773,1777,1780,1787,1803,1814,1818,1824,1835,1841,1845,1851,1854,1864,1868,1871,1874,1877,1881,1887,1890,1908,1922,1929,1938,1952,1966,1968,1981,1990,1992],[11,1710,1711],{},"HashMap 几乎是必问题。只背「数组 + 链表 + 红黑树」只能拿到及格分，能讲清为什么这样设计、并发时具体坏在哪里，才是区分度所在。以下以 JDK 8 的实现为基准，JDK 17\u002F21 的实现主体没有变化。",[136,1713,1715],{"id":1714},"_1-hashmap-的底层结构是什么","1. HashMap 的底层结构是什么？",[11,1717,1718],{},"JDK 8 使用「数组 + 链表 + 红黑树」：数组的每个槽位称为桶，哈希冲突的元素先以链表挂载，链表足够长时转为红黑树。",[11,1720,1721,1722,1725],{},"关键常量：默认容量 16、最大容量 ",[45,1723,1724],{},"1 \u003C\u003C 30","、默认负载因子 0.75、树化阈值 8、退化阈值 6、树化的最小容量 64。",[11,1727,1728,1729,1732,1733,1736],{},"节点是 ",[45,1730,1731],{},"Node\u003CK,V>","，保存 hash、key、value 和 next；树化后变成 ",[45,1734,1735],{},"TreeNode","，额外维护红黑树所需的父子指针和颜色，所以单个节点的内存开销明显更大。",[11,1738,1739],{},"JDK 7 只有数组加链表，且插入使用头插法；JDK 8 改为尾插，并引入红黑树，这是两个版本最值得说明的差异。",[136,1741,1743],{"id":1742},"_2-哈希扰动和下标计算为什么这样设计","2. 哈希扰动和下标计算为什么这样设计？",[11,1745,1746,1747,1750,1751,1754,1755,1758],{},"取 key 的 ",[45,1748,1749],{},"hashCode()"," 后，先做一次扰动：",[45,1752,1753],{},"h ^ (h >>> 16)","，把高 16 位异或到低 16 位。随后用 ",[45,1756,1757],{},"(n - 1) & hash"," 定位桶，其中 n 是数组长度。",[11,1760,1761,1762,1765],{},"因为容量是 2 的幂，",[45,1763,1764],{},"(n - 1)"," 的低位全是 1，与运算等价于取模但更快。问题在于：如果不扰动，只有低位参与定位，高位差异被完全丢弃，大量 key 会挤进少数桶。",[11,1767,1768,1769,1772],{},"扰动让高位也参与运算，以很小的成本改善分布。注意这并不能消除冲突，",[45,1770,1771],{},"hashCode"," 设计糟糕时依旧会退化，所以不要试图用 HashMap 抵抗恶意构造的哈希碰撞。",[136,1774,1776],{"id":1775},"_3-put-的完整流程是什么","3. put 的完整流程是什么？",[11,1778,1779],{},"先判断 table 是否为空，为空则初始化；桶为空就直接放新节点；桶首节点的 hash 相同且 key 相等，就覆盖 value。",[11,1781,1782,1783,1786],{},"否则分两种情况：树节点走 ",[45,1784,1785],{},"putTreeVal","；链表则从头部遍历到尾部，途中发现相等 key 就覆盖，没找到就追加到尾部，并检查链表长度是否达到树化门槛。",[11,1788,1789,1790,1793,1794,131,1797,131,1799,1802],{},"插入完成后，如果元素总数超过阈值 ",[45,1791,1792],{},"容量 × 0.75","，执行扩容。",[45,1795,1796],{},"putIfAbsent",[45,1798,541],{},[45,1800,1801],{},"merge"," 都复用了这套流程，区别只在命中时是否覆盖以及返回值的语义。",[11,1804,1805,1806,1808,1809,571,1811,1813],{},"面试还可以补一句：key 为 ",[45,1807,174],{}," 时固定放在 0 号桶，这也是 ",[45,1810,79],{},[45,1812,125],{}," 的一个行为差异。",[136,1815,1817],{"id":1816},"_4-扩容是怎么做的jdk-7-和-8-有什么差别","4. 扩容是怎么做的？JDK 7 和 8 有什么差别？",[11,1819,1820,1821,1823],{},"扩容时容量翻倍，阈值也翻倍；元素数超过 ",[45,1822,1792],{}," 触发，树化失败但容量不足 64 时也会先扩容。",[11,1825,1826,1827,1830,1831,1834],{},"JDK 8 不需要重新计算哈希：把旧桶拆成低位和高位两条链，判断 ",[45,1828,1829],{},"(e.hash & oldCap)","，为 0 留在原下标 j，否则移到 ",[45,1832,1833],{},"j + oldCap","。这样既省掉重算，又保持了链表内部的相对顺序。",[11,1836,1837,1838,1840],{},"JDK 7 会对每个节点重新计算索引，并且用头插法迁移，并发扩容时可能让两个线程把链表连成环，之后 ",[45,1839,533],{}," 死循环。JDK 8 改尾插后环的问题基本消失，但数据覆盖、size 不准、丢更新依然存在。",[136,1842,1844],{"id":1843},"_5-为什么容量是-2-的幂负载因子为什么是-075","5. 为什么容量是 2 的幂？负载因子为什么是 0.75？",[11,1846,1847,1848,1850],{},"容量为 2 的幂有两个直接好处：",[45,1849,1757],{}," 可以替代取模，位运算更快；扩容时只需看哈希的某一位，就能决定元素留在原位还是移动旧容量个位置。",[11,1852,1853],{},"负载因子 0.75 是时间与空间的折中：调低会减少冲突但浪费空间、增加扩容次数；调高则节省空间但链表变长。源码注释给出的依据是，在 0.75 附近、哈希均匀分布时，单个桶内节点数近似服从参数 0.5 的泊松分布。",[11,1855,1856,1857,1860,1861,300],{},"实际使用时，如果能预估元素数量，直接指定初始容量更有效。注意构造函数会把容量向上取整到 2 的幂，但 ",[45,1858,1859],{},"HashMap(int initialCapacity)"," 并不会自动除以 0.75，所以想放 n 个元素，初始容量应给到 ",[45,1862,1863],{},"n \u002F 0.75 + 1",[136,1865,1867],{"id":1866},"_6-链表树化的阈值为什么是-8退化又是-6","6. 链表树化的阈值为什么是 8，退化又是 6？",[11,1869,1870],{},"树化需要同时满足两个条件：链表长度达到 8，且数组容量不小于 64。后者不满足时只扩容、不树化，因为小表里冲突更多是容量不足造成的。",[11,1872,1873],{},"阈值 8 来自概率：哈希均匀时桶内节点数服从泊松分布，长度达到 8 的概率约为千万分之六，正常情况几乎不会触发。红黑树节点约为普通节点的两倍，只有极端冲突时树化才划算。",[11,1875,1876],{},"退化阈值设为 6 而不是 8，是为了留出缓冲带，避免在 7、8 之间反复树化与退化。节点删除后，当树的结构不足以维持时会退化为链表；扩容拆分时，如果某条链上的节点数不超过 6，也会退回链表。",[136,1878,1880],{"id":1879},"_7-hashmap-为什么线程不安全","7. HashMap 为什么线程不安全？",[11,1882,1883,1884,1886],{},"并发 ",[45,1885,537],{}," 可能同时判断某个桶为空，然后各自写入，后写覆盖先写；两个线程同时扩容时，可能基于旧表复制，导致部分节点丢失或 size 计数错误。",[11,1888,1889],{},"JDK 7 的头插式扩容还可能形成环形链表，CPU 打满；JDK 8 改为尾插并保留相对顺序，环的问题基本不复现，但「不安全」这个结论并没有改变。",[11,1891,1892,1894,1895,1897,1898,1900,1901,1903,1904,1907],{},[45,1893,130],{}," 是方法级 ",[45,1896,693],{},"，粒度太粗；",[45,1899,134],{}," 也是整表锁。并发场景优先 ",[45,1902,125],{},"，它的读基本无锁，写只锁桶，并且通过 ",[45,1905,1906],{},"sizeCtl"," 协调扩容。",[11,1909,1910,1911,1913,1914,1917,1918,985,1920,300],{},"还要注意：即使换成 ",[45,1912,125],{},"，",[45,1915,1916],{},"if (!map.containsKey(k)) map.put(k, v)"," 仍不是原子的，应改用 ",[45,1919,1796],{},[45,1921,541],{},[136,1923,1925,1926,1928],{"id":1924},"_8-作为-key-的对象要满足什么条件如何用-linkedhashmap-实现-lru","8. 作为 key 的对象要满足什么条件？如何用 ",[45,1927,94],{}," 实现 LRU？",[11,1930,1931,1932,364,1935,1937],{},"key 必须正确实现 ",[45,1933,1934],{},"equals",[45,1936,1771],{},"，并且哈希值在放入后保持稳定。如果用可变对象做 key，修改了参与哈希计算的字段，之后按原值就查不到了，等于在表里制造了一个「幽灵条目」。",[11,1939,1940,1942,1943,1946,1947,985,1949,1951],{},[45,1941,94],{}," 在哈希表之上维护一条双向链表。构造时传 ",[45,1944,1945],{},"accessOrder = true","，每次 ",[45,1948,533],{},[45,1950,537],{}," 命中都会把节点移到链表尾部，头部自然成为最久未使用的元素。",[11,1953,1954,1955,1958,1959,1962,1963,1965],{},"再重写 ",[45,1956,1957],{},"removeEldestEntry","，在元素数超过容量时返回 ",[45,1960,1961],{},"true","，就得到一个简单的 LRU。生产环境要注意：访问顺序模式下 ",[45,1964,533],{}," 也会修改结构，遍历时需要额外同步；容量估算、过期时间和并发控制通常还需要外面再包一层。",[136,1967,501],{"id":501},[503,1969,1970,1973,1976],{},[506,1971,1972],{},"「链表长度到 8 就树化」不准确，还要容量至少 64；「缩到 6 就退化」也取决于删除路径与树结构。",[506,1974,1975],{},"初始容量不等于能装多少元素，阈值是容量乘以负载因子。",[506,1977,1978,1980],{},[45,1979,79],{}," 线程不安全的表现不止死循环，数据覆盖和计数错误更常见。",[11,1982,1983,1984,1986,1987,1989],{},"自测：① 手写 ",[45,1985,1757],{}," 并解释为什么等价于取模；② 说明扩容时高低位拆分的判断条件；③ 给出一个用 ",[45,1988,94],{}," 实现 LRU 的最小骨架。",[136,1991,577],{"id":577},[503,1993,1994,2011,2019],{},[506,1995,1996,2001,2002,131,2005,571,2008],{},[583,1997,2000],{"href":1998,"rel":1999},"https:\u002F\u002Fgithub.com\u002Fopenjdk\u002Fjdk\u002Fblob\u002Fmaster\u002Fsrc\u002Fjava.base\u002Fshare\u002Fclasses\u002Fjava\u002Futil\u002FHashMap.java",[587],"HashMap 源码（OpenJDK）"," — 常量、",[45,2003,2004],{},"putVal",[45,2006,2007],{},"resize",[45,2009,2010],{},"treeifyBin",[506,2012,2013,2018],{},[583,2014,2017],{"href":2015,"rel":2016},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F21\u002Fdocs\u002Fapi\u002Fjava.base\u002Fjava\u002Futil\u002FHashMap.html",[587],"java.util.HashMap API（Java 21）"," — 允许 null 键值与非线程安全的官方说明",[506,2020,2021,2026],{},[583,2022,2025],{"href":2023,"rel":2024},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F21\u002Fdocs\u002Fapi\u002Fjava.base\u002Fjava\u002Futil\u002Fconcurrent\u002FConcurrentHashMap.html",[587],"java.util.concurrent.ConcurrentHashMap API"," — 并发语义与复合操作",{"title":613,"searchDepth":614,"depth":614,"links":2028},[2029,2030,2031,2032,2033,2034,2035,2036,2038,2039],{"id":1714,"depth":614,"text":1715},{"id":1742,"depth":614,"text":1743},{"id":1775,"depth":614,"text":1776},{"id":1816,"depth":614,"text":1817},{"id":1843,"depth":614,"text":1844},{"id":1866,"depth":614,"text":1867},{"id":1879,"depth":614,"text":1880},{"id":1924,"depth":614,"text":2037},"8. 作为 key 的对象要满足什么条件？如何用 LinkedHashMap 实现 LRU？",{"id":501,"depth":614,"text":501},{"id":577,"depth":614,"text":577},"沿着 put、扩容和树化三条主线拆解 JDK 8 HashMap，讲清下标计算、阈值设计以及并发场景下的真实风险",{},"30","\u002Fjava-interview\u002Fhashmap",{"title":1706,"description":2040},{"loc":2043},"java-interview\u002Fhashmap","fN16inOHs7Jut5FWs3VOPUasm-kMt8UnJn-239lRsS8",{"id":2049,"title":2050,"body":2051,"category":632,"cover":633,"date":634,"description":2488,"extension":636,"group":637,"meta":2489,"navigation":639,"order":2490,"path":2491,"seo":2492,"sitemap":2493,"stem":2494,"__hash__":2495},"javaInterview\u002Fjava-interview\u002Fjava-basics.md","Java 基础：对象、字符串与泛型",{"type":8,"value":2052,"toc":2473},[2053,2056,2064,2083,2104,2123,2130,2143,2149,2166,2173,2183,2194,2209,2213,2239,2257,2263,2267,2270,2280,2283,2287,2297,2308,2330,2348,2352,2355,2367,2370,2374,2397,2400,2409,2411,2428,2435,2437],[11,2054,2055],{},"基础题最容易答成背诵。下面每题都按「结论 → 原理 → 追问」组织，重点是把 JLS 里的规则和日常踩过的坑对应起来。",[136,2057,139,2059,364,2062,421],{"id":2058},"_1-和-equals-有什么区别",[45,2060,2061],{},"==",[45,2063,1934],{},[11,2065,2066,2068,2069,2071,2072,2075,2076,2078,2079,2082],{},[45,2067,2061],{}," 比较值本身：基本类型比较数值，引用类型比较是否指向同一个对象。",[45,2070,1934],{}," 是 ",[45,2073,2074],{},"Object"," 的方法，默认实现就是 ",[45,2077,2061],{},"，但 ",[45,2080,2081],{},"String","、包装类、集合等重写了它，改成比较内容。",[11,2084,2085,2086,2088,2089,2091,2092,2094,2095,2097,2098,131,2100,2103],{},"重写 ",[45,2087,1934],{}," 必须同时重写 ",[45,2090,1771],{},"，否则两个相等的对象可能算进不同的桶。契约是：",[45,2093,1934],{}," 为真则 ",[45,2096,1771],{}," 必须相等；反过来不要求。违反契约会让 ",[45,2099,79],{},[45,2101,2102],{},"HashSet"," 出现「放进去却查不到」的诡异现象。",[11,2105,2106,2107,2110,2111,2114,2115,2118,2119,2122],{},"追问常见于浮点数：",[45,2108,2109],{},"0.1 + 0.2 == 0.3"," 为 ",[45,2112,2113],{},"false","，因为二进制无法精确表示十进制小数。金额应使用 ",[45,2116,2117],{},"BigDecimal","，并且用字符串构造，否则从 ",[45,2120,2121],{},"double"," 转入时误差已经带入。",[136,2124,2126,2127,2129],{"id":2125},"_2-为什么-string-设计成不可变三种字符串怎么选","2. 为什么 ",[45,2128,2081],{}," 设计成不可变？三种字符串怎么选？",[11,2131,2132,2134,2135,2138,2139,2142],{},[45,2133,2081],{}," 类是 ",[45,2136,2137],{},"final"," 的，内部字符存储在私有字段中（JDK 9 起改为 ",[45,2140,2141],{},"byte[]"," 加编码标记，以压缩拉丁字符占用），所有看似修改的方法都返回新对象。",[11,2144,2145,2146,2148],{},"不可变换来四件事：可以安全共享、可以缓存哈希值（所以适合当 ",[45,2147,79],{}," 的 key）、天然线程安全、传参后不会被调用方篡改。代价是频繁拼接会产生大量临时对象。",[11,2150,2151,2152,2154,2155,2158,2159,2162,2163,2165],{},"选型上：常量或少量拼接用 ",[45,2153,2081],{},"；循环内拼接用 ",[45,2156,2157],{},"StringBuilder","；确实需要多线程共享同一个可变缓冲区时才用 ",[45,2160,2161],{},"StringBuffer","。绝大多数场景里，把 ",[45,2164,2157],{}," 限制在方法内部比用同步版本更有效。",[136,2167,245,2169,2172],{"id":2168},"_3-new-stringabc-会创建几个对象",[45,2170,2171],{},"new String(\"abc\")"," 会创建几个对象？",[11,2174,2175,2176,2179,2180,2182],{},"分情况：若常量池中还没有 ",[45,2177,2178],{},"\"abc\"","，类加载解析字面量时先创建池中对象，执行 ",[45,2181,1161],{}," 时再在堆上创建实例，共两个；若池中已有，则只创建堆上那一个。面试时把两种口径讲清楚，比背「两个」更稳妥。",[11,2184,2185,2188,2189,2110,2192,300],{},[45,2186,2187],{},"intern()"," 的语义是：池中有相等字符串就返回池中引用，否则把当前字符串的引用登记进去并返回。JDK 7 起常量池移到堆中，所以 ",[45,2190,2191],{},"new String(\"a\").intern() == \"a\"",[45,2193,1961],{},[11,2195,2196,2197,2200,2201,2204,2205,2208],{},"编译期能折叠的只有字面量拼接，例如 ",[45,2198,2199],{},"\"a\" + \"b\""," 会直接变成 ",[45,2202,2203],{},"\"ab\"","。而 ",[45,2206,2207],{},"new String(\"a\") + new String(\"b\")"," 是运行期拼接，结果不会自动进入常量池。",[136,2210,2212],{"id":2211},"_4-包装类的缓存与自动拆箱有哪些坑","4. 包装类的缓存与自动拆箱有哪些坑？",[11,2214,2215,2216,2219,2220,2223,2224,2227,2228,2230,2231,2233,2234,2236,2237,300],{},"自动装箱调用 ",[45,2217,2218],{},"Integer.valueOf","，默认缓存 -128 到 127，可用 ",[45,2221,2222],{},"-XX:AutoBoxCacheMax"," 调大上界。因此两个值为 127 的 ",[45,2225,2226],{},"Integer"," 用 ",[45,2229,2061],{}," 比较为 ",[45,2232,1961],{},"，128 则为 ",[45,2235,2113],{},"；比较值一律用 ",[45,2238,1934],{},[11,2240,2241,2242,2245,2246,2249,2250,2252,2253,2256],{},"最常见的坑是空指针：",[45,2243,2244],{},"Integer x = null; x == 1"," 会拆箱，直接抛 NPE。",[45,2247,2248],{},"Map.get()"," 返回 ",[45,2251,174],{}," 后赋给 ",[45,2254,2255],{},"int","、三目表达式两边类型不一致导致整体提升，也都会引发同类问题。",[11,2258,2259,2262],{},[45,2260,2261],{},"Long"," 的缓存范围固定为 -128 到 127，不受上述参数影响。金额、订单号这类业务标识不要依赖缓存行为，统一用值比较或明确的基本类型。",[136,2264,2266],{"id":2265},"_5-java-是值传递还是引用传递","5. Java 是值传递还是引用传递？",[11,2268,2269],{},"Java 只有值传递。基本类型传的是数值副本；引用类型传的是引用的副本，也就是对象地址的拷贝。",[11,2271,2272,2273,2275,2276,2279],{},"所以方法内可以通过这个副本修改对象内容，但把形参重新指向新对象，不会影响调用方的引用。典型例子是：给 ",[45,2274,2157],{}," 追加内容能生效，而在方法里 ",[45,2277,2278],{},"sb = new StringBuilder()"," 之后的操作与外部无关。",[11,2281,2282],{},"追问常考「为什么交换两个对象不生效」：交换的只是两个引用副本。交换数组元素则能生效，因为修改的是数组对象内部的数据。",[136,2284,2286],{"id":2285},"_6-泛型擦除带来了哪些限制pecs-怎么理解","6. 泛型擦除带来了哪些限制？PECS 怎么理解？",[11,2288,2289,2290,2292,2293,2296],{},"Java 泛型是编译期特性，编译后类型参数被擦除到上界（无界时为 ",[45,2291,2074],{},"），运行时拿不到 ",[45,2294,2295],{},"List\u003CString>"," 的具体类型实参。",[11,2298,2299,2300,2303,2304,2307],{},"由此产生几条限制：不能 ",[45,2301,2302],{},"new T()","、不能直接创建泛型数组、不能写 ",[45,2305,2306],{},"instanceof List\u003CString>","、静态成员不能引用类上的类型参数、基本类型不能作为类型实参。为了维持多态，编译器会生成桥方法。",[11,2309,2310,2311,2314,2315,2318,2319,300,2321,2324,2325,2327,2328,436],{},"通配符遵循 PECS：生产者用 ",[45,2312,2313],{},"? extends T","，只能读不能写；消费者用 ",[45,2316,2317],{},"? super T","，可以写但读出来只能是 ",[45,2320,2074],{},[45,2322,2323],{},"List\u003C?>"," 不能 ",[45,2326,199],{}," 任何非 ",[45,2329,174],{},[11,2331,2332,2333,2336,2337,2340,2341,2344,2345,300],{},"还要区分：",[45,2334,2335],{},"List\u003CInteger>"," 不能赋给 ",[45,2338,2339],{},"List\u003CNumber>","，这不是擦除造成的，而是泛型不协变。数组是协变的，所以 ",[45,2342,2343],{},"Object[] arr = new String[1]"," 能通过编译，但写入其他类型会在运行期抛 ",[45,2346,2347],{},"ArrayStoreException",[136,2349,2351],{"id":2350},"_7-重载和重写有什么区别多态靠什么实现","7. 重载和重写有什么区别？多态靠什么实现？",[11,2353,2354],{},"重载在编译期按静态类型和参数列表选定，返回值不同不构成重载；重写在运行期按对象实际类型分派。重写要求签名相同、返回类型协变、访问权限不能收窄、不能抛出更宽的受检异常。",[11,2356,2357,2358,131,2361,131,2364,2366],{},"多态依赖对象头里的类型元数据指针和方法表：调用虚方法时按实际类型找到实现。",[45,2359,2360],{},"private",[45,2362,2363],{},"static",[45,2365,2137],{}," 方法不参与动态分派。",[11,2368,2369],{},"两个容易忽略的点：一是在构造器里调用可重写方法，子类字段尚未初始化，可能读到默认值；二是重载在编译期就固定，父类静态类型看不到子类新增的重载版本。",[136,2371,2373],{"id":2372},"_8-从-java-8-到-21哪些语言特性值得在面试里讲","8. 从 Java 8 到 21，哪些语言特性值得在面试里讲？",[11,2375,2376,2377,2380,2381,2384,2385,2388,2389,2392,2393,2396],{},"JDK 8 带来 Lambda、Stream、",[45,2378,2379],{},"Optional","、接口默认方法和新的日期时间 API。JDK 10 的 ",[45,2382,2383],{},"var"," 只做局部变量类型推断，不改变类型系统。JDK 11 标准化了 ",[45,2386,2387],{},"HttpClient","。JDK 17 是主流 LTS，包含 ",[45,2390,2391],{},"record","、密封类、",[45,2394,2395],{},"instanceof"," 模式匹配、文本块和 switch 表达式。",[11,2398,2399],{},"JDK 21 值得关注 switch 模式匹配（含 record 解构）、虚拟线程、分代 ZGC 和有序集合接口。",[11,2401,2402,2403,2405,2406,2408],{},"回答时最好落到场景：",[45,2404,2391],{}," 适合 DTO 和值对象，不适合需要可变状态或依赖无参构造的框架实体；虚拟线程适合大量阻塞在 IO 上的任务，不适合 CPU 密集任务，也不适合长时间持有 ",[45,2407,693],{}," 的临界区——JDK 21 下会钉住载体线程，JDK 24 才改善这一限制。",[136,2410,501],{"id":501},[503,2412,2413,2416,2425],{},[506,2414,2415],{},"不要只背「String 不可变所以线程安全」，要能说出共享、哈希缓存、常量池三个具体收益。",[506,2417,2418,2419,2421,2422,2424],{},"包装类比较、",[45,2420,174],{}," 拆箱、",[45,2423,2117],{}," 构造方式，是笔试里最高频的失分点。",[506,2426,2427],{},"泛型擦除不等于「运行时完全没有泛型信息」，声明处的信息仍可通过反射读到。",[11,2429,2430,2431,2434],{},"自测：① 解释 ",[45,2432,2433],{},"Integer a = 128; Integer b = 128; a == b"," 的结果；② 说明为什么泛型不能协变而数组可以；③ 说出 JDK 21 虚拟线程不适合哪两类任务。",[136,2436,577],{"id":577},[503,2438,2439,2447,2459,2465],{},[506,2440,2441,2446],{},[583,2442,2445],{"href":2443,"rel":2444},"https:\u002F\u002Fdocs.oracle.com\u002Fjavase\u002Fspecs\u002Fjls\u002Fse21\u002Fhtml\u002Findex.html",[587],"JLS SE 21：类型、转换与泛型"," — 泛型擦除、装箱转换的规范定义",[506,2448,2449,608,2454,571,2456,2458],{},[583,2450,2453],{"href":2451,"rel":2452},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F21\u002Fdocs\u002Fapi\u002Fjava.base\u002Fjava\u002Flang\u002FObject.html",[587],"java.lang.Object API",[45,2455,1934],{},[45,2457,1771],{}," 的契约原文",[506,2460,2461,2464],{},[583,2462,1080],{"href":1078,"rel":2463},[587]," — 虚拟线程的适用场景与钉住问题",[506,2466,2467,2472],{},[583,2468,2471],{"href":2469,"rel":2470},"https:\u002F\u002Fopenjdk.org\u002Fjeps\u002F441",[587],"JEP 441：Pattern Matching for switch"," — switch 模式匹配的正式语义",{"title":613,"searchDepth":614,"depth":614,"links":2474},[2475,2477,2479,2481,2482,2483,2484,2485,2486,2487],{"id":2058,"depth":614,"text":2476},"1. == 和 equals 有什么区别？",{"id":2125,"depth":614,"text":2478},"2. 为什么 String 设计成不可变？三种字符串怎么选？",{"id":2168,"depth":614,"text":2480},"3. new String(\"abc\") 会创建几个对象？",{"id":2211,"depth":614,"text":2212},{"id":2265,"depth":614,"text":2266},{"id":2285,"depth":614,"text":2286},{"id":2350,"depth":614,"text":2351},{"id":2372,"depth":614,"text":2373},{"id":501,"depth":614,"text":501},{"id":577,"depth":614,"text":577},"从 == 与 equals、字符串不可变、包装类缓存、值传递到泛型擦除，梳理 Java 基础题的高频追问和版本边界",{},"10","\u002Fjava-interview\u002Fjava-basics",{"title":2050,"description":2488},{"loc":2491},"java-interview\u002Fjava-basics","Kom7fnDlE7zg-nc90O-2ZtAhpAmc60c451S7-8MzH5k",{"id":2497,"title":2498,"body":2499,"category":632,"cover":633,"date":634,"description":2755,"extension":636,"group":637,"meta":2756,"navigation":639,"order":2757,"path":2758,"seo":2759,"sitemap":2760,"stem":2761,"__hash__":2762},"javaInterview\u002Fjava-interview\u002Fjvm.md","JVM：内存结构、GC 与线上排查",{"type":8,"value":2500,"toc":2743},[2501,2504,2508,2514,2517,2532,2536,2541,2544,2547,2551,2554,2561,2564,2568,2571,2578,2581,2585,2596,2599,2602,2606,2609,2624,2627,2631,2634,2637,2644,2648,2666,2672,2679,2681,2692,2695,2697],[11,2502,2503],{},"JVM 题的典型答法是背名词。更好的方式是把「对象放在哪、什么时候回收、回收时怎么保证正确、出问题怎么取证」这条链路讲通。以下以 HotSpot、JDK 17\u002F21 为语境。",[136,2505,2507],{"id":2506},"_1-jvm-运行时数据区怎么划分","1. JVM 运行时数据区怎么划分？",[11,2509,2510,2511,300],{},"按线程私有和共享划分。私有部分包括程序计数器、虚拟机栈和本地方法栈：程序计数器记录当前字节码位置，是唯一不会 OOM 的区域；虚拟机栈存放栈帧，包含局部变量表、操作数栈和返回地址，递归过深会抛 ",[45,2512,2513],{},"StackOverflowError",[11,2515,2516],{},"共享部分是堆和方法区。堆存放对象实例，是 GC 的主战场；方法区在 HotSpot 中由元空间实现，存放类元数据、运行时常量池等，JDK 8 起从永久代迁到本地内存。",[11,2518,2519,2520,2523,2524,2527,2528,2531],{},"此外还有直接内存，它不属于运行时数据区，但 NIO 的 ",[45,2521,2522],{},"DirectByteBuffer"," 会占用，受 ",[45,2525,2526],{},"-XX:MaxDirectMemorySize"," 影响，超限时报 ",[45,2529,2530],{},"Direct buffer memory","。字符串常量池在 JDK 7 已移到堆中。",[136,2533,2535],{"id":2534},"_2-一个对象从创建到回收经历了什么","2. 一个对象从创建到回收经历了什么？",[11,2537,2538,2540],{},[45,2539,1161],{}," 之后先检查类是否已加载，然后在堆上分配内存。分配通常用指针碰撞或空闲列表，多线程下通过 TLAB 减少竞争；分配后先把实例字段清零，再设置对象头，最后执行构造器。",[11,2542,2543],{},"对象布局包括 Mark Word、类型指针、实例数据和对齐填充。Mark Word 存哈希、GC 年龄和锁状态；开启指针压缩后引用占 4 字节，堆小于约 32GB 时默认开启。",[11,2545,2546],{},"新对象一般进入 Eden。Eden 满时触发 Young GC，存活对象复制到 Survivor 并增加年龄，达到阈值后晋升老年代；大对象可能直接进老年代。老年代压力过大时触发 Full GC 或混合回收。",[136,2548,2550],{"id":2549},"_3-如何判断对象可以回收三色标记解决了什么问题","3. 如何判断对象可以回收？三色标记解决了什么问题？",[11,2552,2553],{},"主流 JVM 用可达性分析：从 GC Roots 出发遍历引用链，不可达的对象才可回收。GC Roots 包括栈上的局部变量、静态字段、JNI 引用、活动线程和同步监视器等。",[11,2555,2556,2557,2560],{},"引用强度分强、软、弱、虚：软引用在内存不足时回收，弱引用下次 GC 就回收，虚引用只用于回收通知。",[45,2558,2559],{},"finalize"," 已废弃，不应作为资源释放手段。",[11,2562,2563],{},"并发标记时用户线程仍在改引用，三色标记可能漏标：黑对象新增了指向白对象的引用，或灰对象到白对象的引用被删除。CMS 用增量更新记录新增引用，G1 用 SATB 记录删除前的快照，两者都靠写屏障实现。",[136,2565,2567],{"id":2566},"_4-g1-和-cmszgc-有什么区别","4. G1 和 CMS、ZGC 有什么区别？",[11,2569,2570],{},"CMS 以低停顿为目标，但存在碎片和并发模式失败，已在 JDK 14 移除。G1 从 JDK 9 起是默认收集器，把堆划分成大小相等的 Region，年轻代和老年代是逻辑集合而非物理连续区域，可以按回收价值优先选择 Region。",[11,2572,2573,2574,2577],{},"G1 有 Young GC、Mixed GC 和必要的 Full GC。",[45,2575,2576],{},"-XX:MaxGCPauseMillis"," 是软目标，不是硬保证；大对象（超过 Region 一半）会进入 Humongous 区域。跨 Region 引用靠记忆集和卡表维护。",[11,2579,2580],{},"ZGC 用染色指针和读屏障实现并发标记与并发整理，停顿与堆大小基本无关，JDK 21 引入分代 ZGC。选型不应只看「谁更新」：吞吐敏感先看 Parallel，延迟敏感看 G1 或 ZGC，还要结合堆大小、CPU 和 JDK 版本压测。",[136,2582,2584],{"id":2583},"_5-类加载过程和双亲委派模型","5. 类加载过程和双亲委派模型？",[11,2586,2587,2588,2591,2592,2595],{},"类的生命周期是加载、链接（验证、准备、解析）、初始化、使用、卸载。准备阶段给静态变量分配内存并设零值，",[45,2589,2590],{},"static final"," 常量在编译期已写入常量池；初始化阶段才执行 ",[45,2593,2594],{},"\u003Cclinit>","，包括静态赋值和静态块。",[11,2597,2598],{},"双亲委派指加载请求先交给父加载器，父加载器无法完成才自己加载。它保证核心类库不被同名类替换，也避免重复加载。启动类加载器、平台类加载器、应用类加载器是三层常见结构。",[11,2600,2601],{},"委派可以被打破：SPI 通过线程上下文类加载器加载实现类；Tomcat 为每个 Web 应用维护独立加载器，实现应用隔离；OSGi 则是网状委派。注意「被动引用」不会触发初始化，例如通过子类访问父类静态字段、访问编译期常量。",[136,2603,2605],{"id":2604},"_6-有哪些-oom线上怎么排查","6. 有哪些 OOM？线上怎么排查？",[11,2607,2608],{},"常见类型：堆空间不足、GC 开销超限、元空间不足（动态生成类过多）、直接内存不足、无法创建本地线程（线程数或栈过大）、数组长度超限。",[11,2610,2611,2612,2615,2616,2619,2620,2623],{},"排查顺序：先确认是哪种 OOM，再看是否有 ",[45,2613,2614],{},"-XX:+HeapDumpOnOutOfMemoryError"," 留下的堆转储；用 ",[45,2617,2618],{},"jcmd \u003Cpid> GC.heap_info"," 看堆使用，用 ",[45,2621,2622],{},"jmap -histo:live"," 看对象分布，必要时导出 hprof 用 MAT 找支配树和 GC Root 路径。",[11,2625,2626],{},"还要区分「内存泄漏」和「内存不够」：前者是对象该回收却仍被引用，需要定位持有链；后者可能是缓存无上限、批量查询过大或堆设置不合理。直接内存和线程数问题不会体现在堆转储里，要看 NMT 和系统级指标。",[136,2628,2630],{"id":2629},"_7-jit-编译和逃逸分析对性能有什么影响","7. JIT 编译和逃逸分析对性能有什么影响？",[11,2632,2633],{},"HotSpot 采用解释器加分层编译：热点方法先由 C1 快速编译，执行更久后交给 C2 做激进优化。方法内联、循环展开、常量折叠、去虚拟化都会显著影响峰值性能，所以短跑基准测试往往失真。",[11,2635,2636],{},"逃逸分析判断对象是否逃出方法或线程。不逃逸的对象可以标量替换，把字段拆成局部变量，从而避免堆分配；锁对象不逃逸时可以做锁消除。它不等于「对象一定分配在栈上」，HotSpot 主要做的是标量替换。",[11,2638,2639,2640,2643],{},"实际影响：压测前要预热，避免把解释执行阶段算进吞吐；不要用过于复杂的继承层次阻碍内联；",[45,2641,2642],{},"-Xint"," 只用于排查，不适合生产。",[136,2645,2647],{"id":2646},"_8-jvm-参数怎么调gc-日志怎么看","8. JVM 参数怎么调？GC 日志怎么看？",[11,2649,2650,2651,364,2654,2657,2658,2661,2662,2665],{},"先定堆：",[45,2652,2653],{},"-Xms",[45,2655,2656],{},"-Xmx"," 设成相同值，避免运行期反复扩缩；容器环境用 ",[45,2659,2660],{},"-XX:MaxRAMPercentage"," 按比例设置，并给堆外内存和元空间留余量。再设 ",[45,2663,2664],{},"-XX:MaxMetaspaceSize","、开启 OOM 堆转储。",[11,2667,2668,2669,2671],{},"GC 参数从业务目标出发：延迟优先可设 ",[45,2670,2576],{},"，但不要设得过小导致 Young 区被压缩、回收更频繁。别照抄网上的「万能参数」，不同 JDK 的默认收集器和日志参数都不一样。",[11,2673,2674,2675,2678],{},"JDK 9 起用统一日志：",[45,2676,2677],{},"-Xlog:gc*:file=gc.log:time,uptime,level,tags","。重点看停顿时间、回收前后占用、晋升量和 Full GC 频率。单次 Young GC 变长可能是存活对象变多，频繁 Full GC 往往要看是不是晋升过快或存在泄漏。",[136,2680,501],{"id":501},[503,2682,2683,2686,2689],{},[506,2684,2685],{},"永久代已在 JDK 8 移除，元空间在本地内存，不等同于「没有上限」。",[506,2687,2688],{},"G1 的 Region 不等于物理连续的新生代\u002F老年代，不要把分代模型硬套到内存布局上。",[506,2690,2691],{},"堆 dump 只能解释堆内问题，直接内存、线程栈、元空间要另找证据。",[11,2693,2694],{},"自测：① 说明 CMS 与 G1 处理漏标的方式差异；② 给出一组容器内 4C8G 服务的 JVM 参数并解释理由；③ 描述一次「堆内存持续上涨」的完整取证步骤。",[136,2696,577],{"id":577},[503,2698,2699,2707,2715,2735],{},[506,2700,2701,2706],{},[583,2702,2705],{"href":2703,"rel":2704},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F21\u002Fgctuning\u002F",[587],"Java SE 21 GC 调优指南"," — 收集器选择、G1 与 ZGC 的停顿目标",[506,2708,2709,2714],{},[583,2710,2713],{"href":2711,"rel":2712},"https:\u002F\u002Fdocs.oracle.com\u002Fjavase\u002Fspecs\u002Fjvms\u002Fse21\u002Fhtml\u002Findex.html",[587],"JVM 规范 SE 21"," — 运行时数据区与类加载的规范定义",[506,2716,2717,608,2722,131,2725,131,2728,131,2731,2734],{},[583,2718,2721],{"href":2719,"rel":2720},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F21\u002Fdocs\u002Fspecs\u002Fman\u002Fjcmd.html",[587],"jcmd 手册（Java 21）",[45,2723,2724],{},"Thread.print",[45,2726,2727],{},"GC.heap_info",[45,2729,2730],{},"GC.heap_dump",[45,2732,2733],{},"JFR.start"," 的语法与影响",[506,2736,2737,2742],{},[583,2738,2741],{"href":2739,"rel":2740},"https:\u002F\u002Fopenjdk.org\u002Fjeps\u002F439",[587],"JEP 439：Generational ZGC"," — 分代 ZGC 的设计目标",{"title":613,"searchDepth":614,"depth":614,"links":2744},[2745,2746,2747,2748,2749,2750,2751,2752,2753,2754],{"id":2506,"depth":614,"text":2507},{"id":2534,"depth":614,"text":2535},{"id":2549,"depth":614,"text":2550},{"id":2566,"depth":614,"text":2567},{"id":2583,"depth":614,"text":2584},{"id":2604,"depth":614,"text":2605},{"id":2629,"depth":614,"text":2630},{"id":2646,"depth":614,"text":2647},{"id":501,"depth":614,"text":501},{"id":577,"depth":614,"text":577},"从运行时数据区、对象布局到三色标记和 G1\u002FZGC 选型，串起 JVM 面试题与 OOM、GC 日志的排查方法",{},"50","\u002Fjava-interview\u002Fjvm",{"title":2498,"description":2755},{"loc":2758},"java-interview\u002Fjvm","dOzS-nwJvTzUqBc3HCiI7ePHVkuafCLBfjYGjvwucN4",{"id":2764,"title":2765,"body":2766,"category":632,"cover":633,"date":634,"description":2988,"extension":636,"group":2989,"meta":2990,"navigation":639,"order":2991,"path":2992,"seo":2993,"sitemap":2994,"stem":2995,"__hash__":2996},"javaInterview\u002Fjava-interview\u002Fmessage-queues.md","消息队列：选型、可靠性与消费幂等",{"type":8,"value":2767,"toc":2976},[2768,2771,2775,2778,2781,2784,2787,2791,2794,2797,2800,2803,2807,2810,2825,2828,2839,2842,2846,2849,2855,2858,2861,2865,2868,2875,2878,2881,2885,2888,2891,2894,2897,2901,2904,2907,2910,2913,2917,2920,2923,2926,2929,2931,2942,2948,2950],[11,2769,2770],{},"消息队列的题很少考 API，更多考「你敢不敢把它放进核心链路」。回答时先讲它带来的三个收益，再讲它引入的四个新问题：一致性、重复、顺序和运维复杂度。",[136,2772,2774],{"id":2773},"_1-为什么要用消息队列引入了哪些问题","1. 为什么要用消息队列？引入了哪些问题？",[11,2776,2777],{},"三个典型收益：解耦（生产者不需要知道有多少下游）、削峰（把突发流量缓冲到消费端能承受的速率）、异步（把非关键步骤移出主链路，缩短响应时间）。",[11,2779,2780],{},"代价同样明显：系统从「一次调用」变成「最终一致」，需要处理消息丢失、重复、乱序和积压；链路变长后，排查问题需要消息 id 贯穿；中间件本身也需要高可用、监控和容量规划。",[11,2782,2783],{},"所以不是所有调用都适合异步。强一致、需要立即返回结果、调用方必须知道成败的场景，同步调用更简单。用 MQ 之前先问：业务能否接受延迟和最终一致？",[11,2785,2786],{},"一个常见反模式是「为了解耦而拆出十几个消费者」，结果每次改消息格式都要协调所有下游，耦合只是从代码转移到了契约。",[136,2788,2790],{"id":2789},"_2-rabbitmqrocketmqkafka-怎么选","2. RabbitMQ、RocketMQ、Kafka 怎么选？",[11,2792,2793],{},"RabbitMQ 基于 Erlang 和 AMQP，路由能力最强，支持 direct、topic、fanout、headers 等交换机模型，单条消息确认和灵活的重试、死信机制成熟，适合业务消息和中小规模异步。",[11,2795,2796],{},"RocketMQ 是 Java 生态，吞吐高，原生支持顺序消息、事务消息、延迟消息和死信队列，比较贴合电商、订单这类业务场景；运维和生态相比 Kafka 稍窄。",[11,2798,2799],{},"Kafka 以分区日志为核心，吞吐最高，适合日志、埋点、流处理和数据管道；它按分区保证顺序，消费位点由客户端管理，缺少原生的单条重试和死信，业务语义要自己补。",[11,2801,2802],{},"选型要看「消息形态」：要复杂路由和逐条确认，选 RabbitMQ；要事务消息和业务顺序，选 RocketMQ；要大吞吐的流式数据，选 Kafka。三者都能做到百万级，不要只拿基准测试的峰值做决定。",[136,2804,2806],{"id":2805},"_3-如何保证消息不丢失","3. 如何保证消息不丢失？",[11,2808,2809],{},"要分三段看：生产、存储、消费。",[11,2811,2812,2813,2816,2817,2820,2821,2824],{},"生产端：RabbitMQ 用 publisher confirm 加持久化交换机\u002F队列；RocketMQ 用同步发送并检查 ",[45,2814,2815],{},"SendStatus","，或事务消息；Kafka 设置 ",[45,2818,2819],{},"acks=all","，配合 ",[45,2822,2823],{},"min.insync.replicas"," 和重试。异步发送必须处理回调失败，不能发出去就不管。",[11,2826,2827],{},"存储端：消息和队列都要持久化，副本数至少 2 到 3，并确认刷盘策略。同步刷盘更安全但更慢，异步刷盘在宕机时可能丢最近一批。",[11,2829,2830,2831,2834,2835,2838],{},"消费端：处理成功后再确认。RabbitMQ 用 ",[45,2832,2833],{},"basicAck","，RocketMQ 返回 ",[45,2836,2837],{},"CONSUME_SUCCESS","，Kafka 在业务完成后提交 offset。先确认后处理，进程崩溃就会丢消息。",[11,2840,2841],{},"即便如此，也只能做到「尽量不丢」，所以关键业务还需要对账和补偿。",[136,2843,2845],{"id":2844},"_4-如何保证消息不被重复消费","4. 如何保证消息不被重复消费？",[11,2847,2848],{},"先明确：端到端严格一次很难做到。生产端重试、网络超时、消费者在「处理成功但确认失败」时重投，都会产生重复，工程上普遍采用「至少一次投递 + 消费端幂等」。",[11,2850,2851,2852,2854],{},"幂等的常见实现：业务唯一键加数据库唯一索引；去重表记录消息 id；Redis ",[45,2853,1532],{}," 做短期去重；状态机只允许特定状态迁移；乐观锁版本号。选择哪种取决于业务，金额类操作更适合唯一约束和流水表。",[11,2856,2857],{},"Kafka 的事务和幂等生产者能保证「Kafka 内部」的写入不重复，但跨数据库的副作用仍要自己做幂等。RocketMQ 的事务消息解决的是「本地事务与发消息」的原子性，不是消费去重。",[11,2859,2860],{},"面试时最好补一句：去重表要设置合理的保留期，并考虑并发下先插入还是先处理。",[136,2862,2864],{"id":2863},"_5-顺序消息怎么实现","5. 顺序消息怎么实现？",[11,2866,2867],{},"顺序的前提是「同一业务键的消息进入同一队列或分区，并且只被一个消费者按序处理」。",[11,2869,2870,2871,2874],{},"Kafka 用消息 key 做分区，同一 key 落到同一 partition，消费者组内一个分区只分配给一个消费者；RocketMQ 用 ",[45,2872,2873],{},"MessageQueueSelector"," 选择队列，消费端用顺序监听器。",[11,2876,2877],{},"破坏顺序的常见原因：生产端重试后消息落到其他分区；消费端多线程并行处理；失败重试把消息挪到队尾；扩容导致分区重新分配。所以顺序消息通常要配合「失败阻塞重试」或把失败消息转入专门的重试队列，避免影响后续消息。",[11,2879,2880],{},"还要评估是否真的需要全局顺序。绝大多数场景只需要「同一订单\u002F同一用户」的局部顺序，全局单分区会牺牲吞吐。",[136,2882,2884],{"id":2883},"_6-消息积压了怎么办","6. 消息积压了怎么办？",[11,2886,2887],{},"先判断原因：消费者挂了、消费变慢（下游数据库或接口）、生产突增、分区数不足，还是消息体过大。",[11,2889,2890],{},"短期止损可以扩容消费者，但 Kafka 的并行度受分区数限制，RocketMQ 受队列数限制，扩容前先确认。必要时临时把积压消息转投到一个分区更多的新 topic，用更多消费者快速消化，再切回正常链路。",[11,2892,2893],{},"消费端可以做批量拉取、批量写库、减少同步远程调用；非核心消息可以降级或延后处理。同时要防止「为了追平而无限并发」把数据库打垮。",[11,2895,2896],{},"长期要做容量评估和监控：Kafka 看 consumer lag，RocketMQ 看堆积量，RabbitMQ 看队列深度。积压告警要带速率和预计消化时间，而不只是一个绝对数字。",[136,2898,2900],{"id":2899},"_7-事务消息和本地消息表怎么选","7. 事务消息和本地消息表怎么选？",[11,2902,2903],{},"RocketMQ 事务消息的流程是：先发半消息，执行本地事务，再根据结果提交或回滚；如果 Broker 长时间收不到确认，会回查生产者的本地事务状态。",[11,2905,2906],{},"它解决的是「本地事务成功但消息没发出去」或「消息发出但本地事务回滚」的问题，适合已有 RocketMQ 且业务链路相对简单的场景。",[11,2908,2909],{},"本地消息表把待发消息和业务数据放在同一个数据库事务里，再由定时任务或 CDC 投递。它不依赖 MQ 的事务能力，跨 MQ 也能用，代价是要多一张表、需要重试和清理，并且投递是异步的。",[11,2911,2912],{},"两者都只能保证最终一致。选择时看团队对中间件的掌握程度、是否已有可靠的调度和对账体系。不要让「事务消息」变成「分布式强一致」的错觉。",[136,2914,2916],{"id":2915},"_8-死信延迟消息和重试应该怎么设计","8. 死信、延迟消息和重试应该怎么设计？",[11,2918,2919],{},"重试要分级：瞬时故障（网络抖动、下游限流）适合指数退避加抖动；业务性失败（参数错误、库存不足）不应无限重试，直接进死信并告警。",[11,2921,2922],{},"延迟消息适合「超时未支付关单」「定时提醒」这类场景。RabbitMQ 可以用 TTL 加死信交换机，或延迟插件；RocketMQ 支持延迟级别和 5.0 的定时消息；Kafka 没有原生延迟，需要外部调度或时间轮。",[11,2924,2925],{},"死信队列不是垃圾桶：要能查询、告警、人工重放，并保留原始消息和失败原因。重放接口要做权限和幂等保护，避免把故障放大。",[11,2927,2928],{},"还要防「毒消息」：某条消息必然处理失败时，如果一直重试会阻塞整个队列。顺序队列尤其要设置最大重试次数，超过后转死信，保证后续消息能继续。",[136,2930,501],{"id":501},[503,2932,2933,2936,2939],{},[506,2934,2935],{},"「消息不丢」是三段式问题，只答 Broker 持久化不完整。",[506,2937,2938],{},"「不重复消费」本质是消费端幂等，别承诺端到端 exactly-once。",[506,2940,2941],{},"顺序、积压、重试三者互相影响，扩容分区可能破坏顺序。",[11,2943,2944,2945,2947],{},"自测：① 画出 RocketMQ 事务消息的半消息与回查流程；② 说明 Kafka 中 ",[45,2946,2819],{}," 仍可能丢消息的条件；③ 给出一个「消费成功但确认失败」的幂等方案。",[136,2949,577],{"id":577},[503,2951,2952,2960,2968],{},[506,2953,2954,2959],{},[583,2955,2958],{"href":2956,"rel":2957},"https:\u002F\u002Fkafka.apache.org\u002Fdocumentation\u002F",[587],"Apache Kafka 官方文档"," — 分区、副本、acks 与事务语义",[506,2961,2962,2967],{},[583,2963,2966],{"href":2964,"rel":2965},"https:\u002F\u002Frocketmq.apache.org\u002Fdocs\u002F",[587],"Apache RocketMQ 官方文档"," — 顺序消息、事务消息、延迟消息与重试",[506,2969,2970,2975],{},[583,2971,2974],{"href":2972,"rel":2973},"https:\u002F\u002Fwww.rabbitmq.com\u002Fdocs\u002Freliability",[587],"RabbitMQ 官方文档：可靠性指南"," — 确认、持久化与死信",{"title":613,"searchDepth":614,"depth":614,"links":2977},[2978,2979,2980,2981,2982,2983,2984,2985,2986,2987],{"id":2773,"depth":614,"text":2774},{"id":2789,"depth":614,"text":2790},{"id":2805,"depth":614,"text":2806},{"id":2844,"depth":614,"text":2845},{"id":2863,"depth":614,"text":2864},{"id":2883,"depth":614,"text":2884},{"id":2899,"depth":614,"text":2900},{"id":2915,"depth":614,"text":2916},{"id":501,"depth":614,"text":501},{"id":577,"depth":614,"text":577},"对比 RabbitMQ、RocketMQ、Kafka 的能力边界，讲清不丢消息、重复消费、顺序、积压和事务消息的工程解法","框架与数据库",{},"100","\u002Fjava-interview\u002Fmessage-queues",{"title":2765,"description":2988},{"loc":2992},"java-interview\u002Fmessage-queues","i2iapbCChmO79fbzpy2OjD9SiP0V3WbUoZOh6x9sk2I",{"id":2998,"title":2999,"body":3000,"category":632,"cover":633,"date":634,"description":3217,"extension":636,"group":1472,"meta":3218,"navigation":639,"order":3219,"path":3220,"seo":3221,"sitemap":3222,"stem":3223,"__hash__":3224},"javaInterview\u002Fjava-interview\u002Fmicroservices.md","微服务：拆分、通信与稳定性治理",{"type":8,"value":3001,"toc":3205},[3002,3005,3009,3012,3015,3018,3021,3025,3028,3031,3034,3037,3041,3044,3047,3050,3053,3057,3060,3063,3066,3085,3089,3092,3095,3098,3101,3105,3108,3111,3114,3117,3121,3124,3127,3134,3137,3141,3144,3147,3150,3153,3155,3166,3169,3171],[11,3003,3004],{},"微服务题的重点不是组件清单，而是「拆不拆、怎么通、出错怎么办」。下面按这三个问题展开，组件只作为实现手段出现。",[136,3006,3008],{"id":3007},"_1-微服务应该怎么拆什么时候不该拆","1. 微服务应该怎么拆？什么时候不该拆？",[11,3010,3011],{},"常见依据是业务能力与限界上下文：一个服务应拥有相对完整的一块业务和它的数据，能独立部署、独立演进。也可以按组织结构和变更频率拆，让经常一起改的代码待在一起。",[11,3013,3014],{},"判断边界时问三个问题：这块能力是否有独立的生命周期？是否有清晰的数据所有权？跨边界的调用是否足够少？如果一个变更总要同时改五个服务，说明边界切错了。",[11,3016,3017],{},"不该拆的情况同样重要：团队规模小、领域还没稳定、强事务要求高、流量很低。此时模块化单体往往更划算，等边界清晰后再拆，比一开始拆错再合并容易。",[11,3019,3020],{},"还要警惕「按技术分层拆」：用户服务、订单服务之外再拆出「查询服务」「写入服务」，会把一次业务变更拉成跨团队协作，收益通常抵不过成本。",[136,3022,3024],{"id":3023},"_2-spring-cloud-常见组件分别解决什么问题","2. Spring Cloud 常见组件分别解决什么问题？",[11,3026,3027],{},"网关负责统一入口、路由、鉴权、限流和灰度；注册中心负责服务实例的注册与发现，常见有 Nacos、Consul、Eureka；客户端负载均衡负责在实例列表中选择目标，Spring Cloud LoadBalancer 已替代 Ribbon。",[11,3029,3030],{},"声明式调用用 OpenFeign，把 HTTP 调用封装成接口；容错限流可以用 Sentinel 或 Resilience4j；配置中心负责动态配置；链路追踪在 Spring Boot 3 时代通常用 Micrometer Tracing 对接 OpenTelemetry。",[11,3032,3033],{},"要注意版本配套：Spring Boot 3 \u002F Spring Cloud 2022+ 使用 Jakarta 命名空间，旧版 Sleuth 已不再是主线。选组件时优先看团队是否真的会用，而不是把全家桶一次性引入。",[11,3035,3036],{},"组件越多，故障面越大。一个只有三个服务的小系统，可能只需要网关、注册中心和基础监控。",[136,3038,3040],{"id":3039},"_3-服务注册发现和负载均衡是怎么工作的","3. 服务注册发现和负载均衡是怎么工作的？",[11,3042,3043],{},"服务启动时把地址、端口、元数据注册到注册中心，并定期发送心跳；消费者拉取或订阅实例列表，缓存在本地，调用时按负载均衡策略选一个实例。",[11,3045,3046],{},"注册中心通常区分临时实例和持久实例：临时实例靠心跳续约，失联后被剔除；持久实例需要显式注销。健康检查有客户端心跳和服务端探测两种方式。",[11,3048,3049],{},"选型会涉及 CAP：Eureka 偏向可用性，允许短时间读到旧列表；ZooKeeper 偏向一致性，网络分区时可能拒绝写；Nacos 默认偏 AP，也提供 CP 模式。",[11,3051,3052],{},"客户端负载均衡不需要额外的代理进程，但每个消费者都要维护实例列表和健康判断。服务端负载均衡（如 Nginx、云负载均衡）把选择集中在一处，运维简单但多一跳。",[136,3054,3056],{"id":3055},"_4-dubbo-和-openfeign-有什么区别spi-有什么不同","4. Dubbo 和 OpenFeign 有什么区别？SPI 有什么不同？",[11,3058,3059],{},"OpenFeign 基于 HTTP，语义通用、调试方便、跨语言友好，适合对外接口和异构系统；代价是文本协议和每次调用的连接、序列化开销相对高。",[11,3061,3062],{},"Dubbo 是 RPC 框架，默认走二进制协议和 TCP 长连接，性能通常更好，提供服务治理能力（超时、重试、路由、负载均衡、熔断）。它更适合内部高频调用，但要求双方使用兼容的协议和序列化方式。",[11,3064,3065],{},"Dubbo 3 支持 Triple 协议，兼容 gRPC 和 HTTP\u002F2，跨语言能力比早期版本好很多。选型不能只看性能，还要看团队对协议、序列化和治理的掌握程度。",[11,3067,3068,3069,3072,3073,3076,3077,3080,3081,3084],{},"SPI 方面，JDK SPI 通过 ",[45,3070,3071],{},"META-INF\u002Fservices"," 加载，一次性实例化所有实现，无法按需选择；Dubbo SPI 用 ",[45,3074,3075],{},"@SPI"," 指定默认实现，",[45,3078,3079],{},"@Adaptive"," 做自适应扩展，",[45,3082,3083],{},"@Activate"," 做条件激活，支持按名称获取和包装扩展，灵活得多。",[136,3086,3088],{"id":3087},"_5-超时重试熔断限流应该怎么配","5. 超时、重试、熔断、限流应该怎么配？",[11,3090,3091],{},"超时应该自下而上设置：下游超时小于上游，并留出处理余量。更好的做法是传递截止时间（deadline），让整条调用链共享同一个预算，而不是每层各自设一个固定值。",[11,3093,3094],{},"重试只对幂等操作开启，并且要限制次数、加退避和抖动。同步调用链上层层重试会造成放大效应，三层各重试两次就是八倍流量，容易把已经过载的下游彻底压垮。",[11,3096,3097],{},"熔断器有闭合、打开、半开三个状态：失败率超阈值后打开，快速失败；经过冷却期进入半开，放少量请求试探。它保护的是调用方，也给了下游恢复时间。",[11,3099,3100],{},"限流常用令牌桶（允许突发）和漏桶（平滑输出），还要考虑单机限流与集群限流的差别。降级方案要提前设计好返回值，不能等故障时临时决定。",[136,3102,3104],{"id":3103},"_6-分布式事务有哪些方案seata-at-的边界在哪","6. 分布式事务有哪些方案？Seata AT 的边界在哪？",[11,3106,3107],{},"强一致方案主要是 2PC\u002FXA：协调者统一提交或回滚，实现简单但有同步阻塞、协调者单点和性能问题。3PC 缓解阻塞，仍不能很好应对网络分区。",[11,3109,3110],{},"柔性事务包括 TCC、Saga、本地消息表和可靠消息。TCC 需要业务实现 Try\u002FConfirm\u002FCancel，要处理空回滚、悬挂和幂等；Saga 用一系列本地事务加补偿，隔离性弱，适合长流程；本地消息表靠最终一致，工程上最常用。",[11,3112,3113],{},"Seata AT 通过解析 SQL 生成前后镜像，提交本地事务并记录 undo log，失败时反向补偿。它默认不提供强隔离，读到中间状态需要配合全局锁或业务设计；复杂 SQL、存储过程、非事务表都可能失效。",[11,3115,3116],{},"选择方案的顺序应该是：能不能不分布式事务（合并服务、调整边界）→ 能否最终一致 → 最后才考虑强一致方案。",[136,3118,3120],{"id":3119},"_7-微服务可观测性要建设哪些能力","7. 微服务可观测性要建设哪些能力？",[11,3122,3123],{},"三根支柱是日志、指标和链路追踪。日志要结构化，并带上 traceId、用户标识和业务单号；指标关注流量、错误率、延迟和饱和度；链路追踪要能串起跨服务调用，定位是哪一跳变慢。",[11,3125,3126],{},"指标命名可以套用 RED（请求速率、错误、耗时）和 USE（使用率、饱和度、错误）；告警要基于症状（用户可感知的错误率和延迟），而不是只看 CPU。",[11,3128,3129,3130,3133],{},"OpenTelemetry 正在成为事实标准，trace context 通过 W3C ",[45,3131,3132],{},"traceparent"," 头传递。采样策略要平衡成本与可观测性，错误请求和慢请求应提高采样率。",[11,3135,3136],{},"可观测性不是装个 APM 就结束：要有统一的检索入口、清晰的仪表盘和可执行的 Runbook，否则故障时仍然靠猜。",[136,3138,3140],{"id":3139},"_8-网关层要做哪些事鉴权怎么设计","8. 网关层要做哪些事？鉴权怎么设计？",[11,3142,3143],{},"网关适合做与业务无关的横切能力：路由转发、TLS 终止、统一鉴权、限流熔断、跨域、请求日志、灰度分流和协议转换。它不适合承载业务逻辑，否则会成为新的单体。",[11,3145,3146],{},"鉴权可以在网关做粗粒度的身份校验（验签、解析 token、拦截未登录请求），细粒度的权限判断仍应放在服务内，因为服务需要知道「这个用户能否操作这个资源」。",[11,3148,3149],{},"JWT 自带声明、便于水平扩展，但撤销困难；常见做法是短有效期加刷新令牌，敏感操作再查一次权限或黑名单。传统 Session 依赖集中存储，撤销容易但需要共享会话。",[11,3151,3152],{},"内部服务之间建议使用 mTLS 或服务身份令牌，不要默认「内网可信」。网关还要防止把外部请求头原样透传，避免伪造身份字段。",[136,3154,501],{"id":501},[503,3156,3157,3160,3163],{},[506,3158,3159],{},"拆分不是越细越好，数据所有权和变更边界比「服务数量」更重要。",[506,3161,3162],{},"重试是放大器，没有幂等和退避的重试是故障加速器。",[506,3164,3165],{},"分布式事务没有银弹，先问业务能否接受最终一致。",[11,3167,3168],{},"自测：① 说明客户端负载均衡与服务端负载均衡的差异；② 画出熔断器的三个状态及迁移条件；③ 解释 Seata AT 为什么默认不是强隔离。",[136,3170,577],{"id":577},[503,3172,3173,3181,3189,3197],{},[506,3174,3175,3180],{},[583,3176,3179],{"href":3177,"rel":3178},"https:\u002F\u002Fspring.io\u002Fprojects\u002Fspring-cloud",[587],"Spring Cloud 官方文档"," — 各组件与版本对应关系",[506,3182,3183,3188],{},[583,3184,3187],{"href":3185,"rel":3186},"https:\u002F\u002Fdubbo.apache.org\u002Fen\u002Foverview\u002F",[587],"Apache Dubbo 官方文档"," — 协议、SPI 与服务治理",[506,3190,3191,3196],{},[583,3192,3195],{"href":3193,"rel":3194},"https:\u002F\u002Fseata.apache.org\u002Fdocs\u002Foverview\u002Fwhat-is-seata\u002F",[587],"Seata 官方文档"," — AT、TCC、Saga 的模式与约束",[506,3198,3199,3204],{},[583,3200,3203],{"href":3201,"rel":3202},"https:\u002F\u002Fopentelemetry.io\u002Fdocs\u002F",[587],"OpenTelemetry 文档"," — 链路、指标与日志的统一采集",{"title":613,"searchDepth":614,"depth":614,"links":3206},[3207,3208,3209,3210,3211,3212,3213,3214,3215,3216],{"id":3007,"depth":614,"text":3008},{"id":3023,"depth":614,"text":3024},{"id":3039,"depth":614,"text":3040},{"id":3055,"depth":614,"text":3056},{"id":3087,"depth":614,"text":3088},{"id":3103,"depth":614,"text":3104},{"id":3119,"depth":614,"text":3120},{"id":3139,"depth":614,"text":3140},{"id":501,"depth":614,"text":501},{"id":577,"depth":614,"text":577},"从服务边界、Spring Cloud 组件到 Dubbo、超时重试与分布式事务，梳理微服务面试的主线与常见误区",{},"110","\u002Fjava-interview\u002Fmicroservices",{"title":2999,"description":3217},{"loc":3220},"java-interview\u002Fmicroservices","aRBlrkBh77MOx-xHJaG2vYyVVzsaJVAgpGyBGmW23ew",{"id":3226,"title":3227,"body":3228,"category":632,"cover":633,"date":634,"description":3661,"extension":636,"group":2989,"meta":3662,"navigation":639,"order":3663,"path":3664,"seo":3665,"sitemap":3666,"stem":3667,"__hash__":3668},"javaInterview\u002Fjava-interview\u002Fmybatis.md","MyBatis：映射原理、缓存与批量处理",{"type":8,"value":3229,"toc":3647},[3230,3233,3242,3259,3264,3281,3288,3292,3302,3323,3330,3340,3344,3358,3364,3371,3379,3396,3406,3413,3416,3420,3447,3450,3465,3471,3475,3497,3500,3518,3522,3525,3528,3531,3534,3538,3544,3558,3580,3591,3593,3609,3612,3614],[11,3231,3232],{},"MyBatis 的面试重点不是会写 CRUD，而是理解它如何在「手写 SQL」和「对象映射」之间做取舍，以及这些设计在事务、缓存和批量场景下的副作用。",[136,3234,139,3236,364,3239,421],{"id":3235},"_1-和-有什么区别",[45,3237,3238],{},"#{}",[45,3240,3241],{},"${}",[11,3243,3244,3246,3247,3250,3251,3254,3255,3258],{},[45,3245,3238],{}," 会被编译成 ",[45,3248,3249],{},"PreparedStatement"," 的占位符 ",[45,3252,3253],{},"?","，参数通过 ",[45,3256,3257],{},"TypeHandler"," 绑定，能防止 SQL 注入，也能复用执行计划，应该作为传值的默认选择。",[11,3260,3261,3263],{},[45,3262,3241],{}," 是字符串直接替换，发生在 SQL 解析之前。它适合表名、列名、排序字段这类无法用占位符表达的位置，但必须对输入做白名单校验，否则就是注入漏洞。",[11,3265,3266,3267,3269,3270,3272,3273,3276,3277,3280],{},"一个常见误区是「",[45,3268,3238],{}," 会加引号，",[45,3271,3241],{}," 不会」——本质区别是预编译绑定与文本拼接，引号只是表象。",[45,3274,3275],{},"order by #{column}"," 会变成 ",[45,3278,3279],{},"order by 'name'","，排序不生效，正是这个原因。",[11,3282,3283,3284,3287],{},"另外，可空参数在部分数据库（如 Oracle）需要显式 ",[45,3285,3286],{},"jdbcType","，否则可能报无效列类型；MySQL 通常不强制要求。",[136,3289,3291],{"id":3290},"_2-mapper-接口没有实现类为什么能调用","2. Mapper 接口没有实现类，为什么能调用？",[11,3293,3294,3295,3298,3299,300],{},"启动时 MyBatis 扫描 Mapper 接口，把每个方法解析成 ",[45,3296,3297],{},"MappedStatement","，以「命名空间 + 方法名」为 key 存入 ",[45,3300,3301],{},"Configuration",[11,3303,3304,3305,3308,3309,3312,3313,131,3316,131,3319,3322],{},"调用时通过 ",[45,3306,3307],{},"MapperProxyFactory"," 生成 JDK 动态代理。",[45,3310,3311],{},"MapperProxy"," 拦截方法调用，根据方法签名找到对应语句，转换成 ",[45,3314,3315],{},"SqlSession.selectOne",[45,3317,3318],{},"selectList",[45,3320,3321],{},"insert"," 等操作。",[11,3324,3325,3326,3329],{},"所以接口只是「声明」，SQL 的真正来源是 XML 或注解。方法重载在 MyBatis 中通常不被支持，因为语句 id 只由方法名决定；参数名在编译时若没保留，需要用 ",[45,3327,3328],{},"@Param"," 显式命名。",[11,3331,3332,3333,3335,3336,3339],{},"理解这一点也能解释为什么 Mapper 不能被随意 ",[45,3334,1161],{},"：它依赖 ",[45,3337,3338],{},"SqlSession"," 和配置上下文，脱离容器创建出来的代理无法工作。",[136,3341,3343],{"id":3342},"_3-一级缓存和二级缓存是怎么回事生产环境要不要开","3. 一级缓存和二级缓存是怎么回事？生产环境要不要开？",[11,3345,3346,3347,3349,3350,3353,3354,3357],{},"一级缓存是 ",[45,3348,3338],{}," 级别的，默认开启，底层是一个 ",[45,3351,3352],{},"PerpetualCache","。同一会话中相同的语句和参数会命中缓存，执行增删改或提交、回滚后失效。",[45,3355,3356],{},"localCacheScope=STATEMENT"," 可以让它只在单条语句内生效。",[11,3359,3360,3361,3363],{},"与 Spring 集成后，如果方法没有事务，每次 Mapper 调用都可能使用新的 ",[45,3362,3338],{},"，一级缓存几乎不命中；处于同一事务时才会共享。",[11,3365,3366,3367,3370],{},"二级缓存是命名空间级别，需要显式配置 ",[45,3368,3369],{},"\u003Ccache\u002F>","，跨会话共享，要求结果可序列化。它在多节点部署下容易读到陈旧数据，写操作也只清空当前命名空间的缓存，因此生产环境通常建议关闭，改用 Redis 等集中式缓存。",[136,3372,3374,3375,3378],{"id":3373},"_4-resultmap-和-n1-问题怎么处理","4. ",[45,3376,3377],{},"resultMap"," 和 N+1 问题怎么处理？",[11,3380,3381,3383,3384,3387,3388,3391,3392,3395],{},[45,3382,3377],{}," 负责列到属性的映射，支持 ",[45,3385,3386],{},"association","（一对一）和 ",[45,3389,3390],{},"collection","（一对多）。它能处理下划线转驼峰、类型转换和复杂嵌套，比 ",[45,3393,3394],{},"resultType"," 的自动映射更可控。",[11,3397,3398,3399,3401,3402,3405],{},"嵌套结果有两种写法：一种是 join 一次查回，用 ",[45,3400,3377],{}," 的 id 标签合并重复行；另一种是「嵌套查询」，主查询后再按 id 发子查询，配置 ",[45,3403,3404],{},"fetchType=\"lazy\""," 可以延迟加载。",[11,3407,3408,3409,3412],{},"后者就是 N+1 的来源：查 100 条订单会再发 100 次明细查询。解决思路是 join 一次查回，或先收集主键再用 ",[45,3410,3411],{},"IN"," 批量查、在内存里组装。延迟加载只能推迟开销，不能消除次数。",[11,3414,3415],{},"还要注意 join 一对多时结果集行数会膨胀，分页要谨慎，先分页主表再查明细通常更稳。",[136,3417,3419],{"id":3418},"_5-插件机制是怎么实现的分页插件原理是什么","5. 插件机制是怎么实现的？分页插件原理是什么？",[11,3421,3422,3423,131,3426,131,3429,131,3432,3435,3436,364,3439,3442,3443,3446],{},"MyBatis 允许拦截四类对象的方法：",[45,3424,3425],{},"Executor",[45,3427,3428],{},"StatementHandler",[45,3430,3431],{},"ParameterHandler",[45,3433,3434],{},"ResultSetHandler","。插件通过 ",[45,3437,3438],{},"@Intercepts",[45,3440,3441],{},"@Signature"," 声明拦截点，再由 ",[45,3444,3445],{},"InterceptorChain"," 逐层包装成动态代理。",[11,3448,3449],{},"多个插件形成责任链，顺序由配置决定；包装的是同一批核心对象，所以顺序会影响 SQL 改写和参数处理的结果。",[11,3451,3452,3453,3456,3457,3460,3461,3464],{},"分页插件通常在 ",[45,3454,3455],{},"Executor.query"," 处拦截：先根据原 SQL 生成 ",[45,3458,3459],{},"count"," 语句，再改写为带 ",[45,3462,3463],{},"limit"," 的语句，最后把结果包装成分页对象。有的实现依赖方言识别，复杂 SQL 的 count 优化需要谨慎。",[11,3466,3467,3468,3470],{},"使用分页插件时要注意：分页参数常放在 ",[45,3469,934],{}," 中，必须「设置后立即查询」，中间插入其他查询会串页；异步线程里也不会自动传递。",[136,3472,3474],{"id":3473},"_6-大批量写入和超大结果集怎么处理","6. 大批量写入和超大结果集怎么处理？",[11,3476,3477,3478,3481,3482,3484,3485,3488,3489,3492,3493,3496],{},"批量写入有三条路：在 ",[45,3479,3480],{},"foreach"," 中拼多值 ",[45,3483,3321],{},"；使用 ",[45,3486,3487],{},"ExecutorType.BATCH"," 并定期 ",[45,3490,3491],{},"flushStatements","；或交给上层做分批提交。MySQL 连接串加 ",[45,3494,3495],{},"rewriteBatchedStatements=true"," 才能把批量语句真正合并发送。",[11,3498,3499],{},"批处理要注意事务大小：一次提交几十万行会撑大 undo、拉长锁持有时间。按几百到几千条分批，并评估失败重试的幂等性。",[11,3501,3502,3503,3505,3506,3509,3510,3513,3514,3517],{},"超大结果集不要用 ",[45,3504,3318],{}," 一次装进内存。可以用 ",[45,3507,3508],{},"ResultHandler"," 逐行处理，或用 ",[45,3511,3512],{},"Cursor"," 流式读取，同时把 ",[45,3515,3516],{},"fetchSize"," 设为合适的值；MySQL 驱动需要流式模式才真正逐行拉取，连接不能被其他查询复用。",[136,3519,3521],{"id":3520},"_7-mybatis-和-jpa-hibernate-怎么选","7. MyBatis 和 JPA \u002F Hibernate 怎么选？",[11,3523,3524],{},"MyBatis 把 SQL 控制权交给开发者，适合复杂查询、遗留库、需要精细优化的场景；代价是重复的映射和手写 SQL，简单 CRUD 显得啰嗦。",[11,3526,3527],{},"JPA\u002FHibernate 以对象模型为中心，自动生成 SQL，开发效率高，但复杂查询容易失控，懒加载和缓存的行为需要经验才能驾驭。",[11,3529,3530],{},"实际项目里常见混合方案：核心写操作和复杂报表用 MyBatis，后台管理等简单 CRUD 用 JPA 或 MyBatis-Plus。MyBatis-Plus 在 MyBatis 之上提供通用 CRUD、条件构造器和分页，但复杂 SQL 仍要手写，别把它当成完全的 ORM。",[11,3532,3533],{},"选型的关键不是「哪个更先进」，而是团队能否为生成出来的 SQL 负责。",[136,3535,3537],{"id":3536},"_8-日常使用中有哪些容易踩的坑","8. 日常使用中有哪些容易踩的坑？",[11,3539,3540,3541,3543],{},"参数名：Spring Boot 编译默认保留参数名，但依赖构建配置；多参数方法建议显式 ",[45,3542,3328],{},"，避免升级 JDK 或改编译参数后失效。",[11,3545,3546,3547,3550,3551,571,3553,3555,3556,300],{},"映射：开启 ",[45,3548,3549],{},"mapUnderscoreToCamelCase"," 后仍要注意别名覆盖；",[45,3552,3394],{},[45,3554,3377],{}," 不要混用；枚举、JSON 字段需要自定义 ",[45,3557,3257],{},[11,3559,3560,3561,3564,3565,230,3568,3571,3572,3575,3576,3579],{},"查询：",[45,3562,3563],{},"selectOne"," 返回多行会抛 ",[45,3566,3567],{},"TooManyResultsException",[45,3569,3570],{},"\u003Cif test=\"flag == 'Y'\">"," 在 OGNL 中比较的是字符，字符串判断应写成 ",[45,3573,3574],{},"\u003Cif test='flag == \"Y\"'>","，或使用 ",[45,3577,3578],{},"'Y'.toString().equals(flag)"," 这类明确写法。",[11,3581,3582,3583,3586,3587,3590],{},"事务与连接：批处理、流式查询会长时间占用连接；多数据源要确认事务管理器和 ",[45,3584,3585],{},"SqlSessionFactory"," 是否配对；",[45,3588,3589],{},"allowMultiQueries"," 打开后要评估注入风险。",[136,3592,501],{"id":501},[503,3594,3595,3603,3606],{},[506,3596,3597,3599,3600,3602],{},[45,3598,3241],{}," 不是「不加引号的 ",[45,3601,3238],{},"」，而是文本替换，任何来自用户的输入都不能直接拼。",[506,3604,3605],{},"二级缓存是命名空间级、跨会话的，分布式下默认不要开。",[506,3607,3608],{},"延迟加载解决的是「何时查」，不是「查多少次」，N+1 仍要改成批量或 join。",[11,3610,3611],{},"自测：① 描述一次 Mapper 方法调用从代理到 JDBC 的完整链路；② 写出批量插入的两种实现并说明事务边界；③ 解释为什么分页插件在异步线程里会失效。",[136,3613,577],{"id":577},[503,3615,3616,3624,3636],{},[506,3617,3618,3623],{},[583,3619,3622],{"href":3620,"rel":3621},"https:\u002F\u002Fmybatis.org\u002Fmybatis-3\u002F",[587],"MyBatis 3 官方文档"," — 动态 SQL、resultMap、缓存与插件",[506,3625,3626,608,3631,131,3633,3635],{},[583,3627,3630],{"href":3628,"rel":3629},"https:\u002F\u002Fgithub.com\u002Fmybatis\u002Fmybatis-3",[587],"MyBatis 源码（GitHub）",[45,3632,3311],{},[45,3634,3425],{}," 与拦截链实现",[506,3637,3638,608,3643,3646],{},[583,3639,3642],{"href":3640,"rel":3641},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Fconnector-j\u002Fen\u002Fconnector-j-reference-configuration-properties.html",[587],"MySQL Connector\u002FJ 配置",[45,3644,3645],{},"rewriteBatchedStatements"," 与流式结果集参数",{"title":613,"searchDepth":614,"depth":614,"links":3648},[3649,3651,3652,3653,3655,3656,3657,3658,3659,3660],{"id":3235,"depth":614,"text":3650},"1. #{} 和 ${} 有什么区别？",{"id":3290,"depth":614,"text":3291},{"id":3342,"depth":614,"text":3343},{"id":3373,"depth":614,"text":3654},"4. resultMap 和 N+1 问题怎么处理？",{"id":3418,"depth":614,"text":3419},{"id":3473,"depth":614,"text":3474},{"id":3520,"depth":614,"text":3521},{"id":3536,"depth":614,"text":3537},{"id":501,"depth":614,"text":501},{"id":577,"depth":614,"text":577},"讲清 #{} 与 ${} 的区别、Mapper 动态代理、一二级缓存、N+1 与插件机制，并给出大数据量读写和常见踩坑的解法",{},"70","\u002Fjava-interview\u002Fmybatis",{"title":3227,"description":3661},{"loc":3664},"java-interview\u002Fmybatis","r2lQvwo2OMwsIhOfbRNZLjYo0F6OORf-USm8Lr5dzH4",{"id":3670,"title":3671,"body":3672,"category":632,"cover":633,"date":634,"description":4134,"extension":636,"group":2989,"meta":4135,"navigation":639,"order":4136,"path":4137,"seo":4138,"sitemap":4139,"stem":4140,"__hash__":4141},"javaInterview\u002Fjava-interview\u002Fmysql.md","MySQL：索引、事务与慢查询优化",{"type":8,"value":3673,"toc":4121},[3674,3677,3752,3756,3759,3762,3765,3768,3772,3782,3785,3807,3810,3814,3827,3841,3858,3861,3865,3868,3871,3883,3886,3890,3893,3896,3899,3912,3918,3961,3983,3990,4000,4004,4010,4013,4035,4042,4045,4049,4052,4055,4058,4061,4063,4074,4077,4079,4119],[11,3675,3676],{},"MySQL 题如果只答「加索引」，很难拿到高分。面试官更想听的是：为什么这条 SQL 用不上索引、这条更新加了什么锁、RR 到底解决了什么又没解决什么。以下默认 InnoDB、MySQL 8.0。",[15,3678,3679,3692],{},[18,3680,3681],{},[21,3682,3683,3686,3689],{},[24,3684,3685],{},"场景",[24,3687,3688],{},"关键机制",[24,3690,3691],{},"常见误判",[34,3693,3694,3705,3716,3730,3741],{},[21,3695,3696,3699,3702],{},[39,3697,3698],{},"按主键查一行",[39,3700,3701],{},"聚簇索引，叶子即行数据",[39,3703,3704],{},"以为二级索引也要回表",[21,3706,3707,3710,3713],{},[39,3708,3709],{},"按普通索引查整行",[39,3711,3712],{},"二级索引 → 主键 → 回表",[39,3714,3715],{},"忽略回表成本",[21,3717,3718,3721,3724],{},[39,3719,3720],{},"查询列都在索引里",[39,3722,3723],{},"覆盖索引，免回表",[39,3725,3726,3727],{},"以为必须 ",[45,3728,3729],{},"SELECT *",[21,3731,3732,3735,3738],{},[39,3733,3734],{},"范围条件 + 索引过滤",[39,3736,3737],{},"索引下推（ICP）",[39,3739,3740],{},"以为过滤一定回表后做",[21,3742,3743,3746,3749],{},[39,3744,3745],{},"高并发读写同一行",[39,3747,3748],{},"行锁 + 一致性读",[39,3750,3751],{},"以为 RR 下读都不加锁",[136,3753,3755],{"id":3754},"_1-innodb-的索引结构是什么聚簇索引和二级索引有什么区别","1. InnoDB 的索引结构是什么？聚簇索引和二级索引有什么区别？",[11,3757,3758],{},"InnoDB 用 B+ 树组织索引。主键索引是聚簇索引，叶子节点直接存放整行数据，所以按主键查最快；如果没有主键，会选第一个非空唯一索引，再没有就生成隐藏的行 ID。",[11,3760,3761],{},"二级索引的叶子存放的是索引列加主键值。查整行时需要先查二级索引拿到主键，再回聚簇索引取数据，这个动作叫回表，也是「索引很多但查询仍然慢」的常见原因。",[11,3763,3764],{},"B+ 树的非叶子节点只存键和指针，扇出大、层数少，通常三到四层就能覆盖千万级数据。页大小默认 16KB，顺序主键插入能减少页分裂，随机 UUID 主键则会带来更多分裂和碎片。",[11,3766,3767],{},"MyISAM 的索引和数据分离，索引叶子存的是行地址，没有聚簇概念，也不支持事务和行锁。",[136,3769,3771],{"id":3770},"_2-哪些写法会导致索引失效","2. 哪些写法会导致索引失效？",[11,3773,3774,3775,131,3778,3781],{},"最常见的是在索引列上做函数或运算，例如 ",[45,3776,3777],{},"WHERE DATE(create_time) = '2026-10-01'",[45,3779,3780],{},"WHERE id + 1 = 10","。应改成范围条件，或建立函数索引（MySQL 8.0.13 起支持）。",[11,3783,3784],{},"隐式类型转换也会失效：字符串列与数字比较时，MySQL 会把列转成数字，索引无法使用。参数类型要和列定义一致。",[11,3786,3787,3788,3791,3792,3795,3796,131,3799,3802,3803,3806],{},"其他情况包括：",[45,3789,3790],{},"LIKE '%关键字'"," 前导通配；复合索引不满足最左前缀；",[45,3793,3794],{},"OR"," 两侧有一侧没有索引；",[45,3797,3798],{},"!=",[45,3800,3801],{},"NOT IN"," 在选择性差时被优化器放弃；对索引列使用 ",[45,3804,3805],{},"IS NULL"," 并非一定失效，要看数据分布。",[11,3808,3809],{},"最后要理解「失效」很多时候是优化器的成本选择：当回表行数占比很高，全表扫描反而更便宜，强制走索引未必更快。",[136,3811,3813],{"id":3812},"_3-什么是覆盖索引和索引下推","3. 什么是覆盖索引和索引下推？",[11,3815,3816,3817,530,3820,3823,3824,300],{},"覆盖索引指查询需要的列全部能在索引里拿到，不需要回表。",[45,3818,3819],{},"EXPLAIN",[45,3821,3822],{},"Extra"," 会显示 ",[45,3825,3826],{},"Using index",[11,3828,3829,3830,3833,3834,3837,3838,3840],{},"例如给 ",[45,3831,3832],{},"(user_id, status, created_at)"," 建联合索引，查询 ",[45,3835,3836],{},"SELECT status, created_at FROM orders WHERE user_id = ?"," 就能覆盖；如果再加 ",[45,3839,3729],{},"，就得回表。",[11,3842,3843,3844,3847,3848,3851,3852,3854,3855,300],{},"索引下推（ICP）是在存储引擎层用索引中的列提前过滤，减少回表次数。例如联合索引 ",[45,3845,3846],{},"(name, age)","，条件 ",[45,3849,3850],{},"WHERE name LIKE '张%' AND age = 20","，age 可以在索引上先判断，",[45,3853,3822],{}," 显示 ",[45,3856,3857],{},"Using index condition",[11,3859,3860],{},"覆盖索引和索引下推解决的是不同问题：前者省掉回表，后者减少回表。设计联合索引时，把等值条件放前面、范围条件放后面，通常能同时兼顾过滤和排序。",[136,3862,3864],{"id":3863},"_4-mvcc-是怎么实现的rr-能完全解决幻读吗","4. MVCC 是怎么实现的？RR 能完全解决幻读吗？",[11,3866,3867],{},"InnoDB 每行有隐藏的事务 ID 和回滚指针，回滚指针把旧版本串成 undo 链。读取时根据 ReadView 判断哪个版本可见：当前事务 ID 小于最小活跃事务 ID 的版本可见，大于等于最大事务 ID 的不可见，处于活跃列表中的也不可见。",[11,3869,3870],{},"RC 每条语句生成一个新的 ReadView，所以能读到别人已提交的修改；RR 在事务第一次快照读时生成，之后复用，因此同一事务内多次查询结果一致。",[11,3872,3873,3874,131,3876,131,3879,3882],{},"但 RR 不能在所有场景下消除幻读：快照读靠一致性视图，当前读（",[45,3875,1559],{},[45,3877,3878],{},"UPDATE",[45,3880,3881],{},"DELETE","）会读最新版本并加锁。如果先快照读、再当前读，或者两个事务以不同顺序读写，仍可能看到「幻影」。",[11,3884,3885],{},"间隙锁和 next-key lock 是 RR 下当前读防幻读的手段，代价是更大的锁范围和更高的死锁概率。",[136,3887,3889],{"id":3888},"_5-innodb-有哪些锁间隙锁和-next-key-lock-怎么用","5. InnoDB 有哪些锁？间隙锁和 next-key lock 怎么用？",[11,3891,3892],{},"按粒度分表锁、行锁和意向锁；按模式分共享锁和排他锁。行锁实际加在索引记录上，所以「没有索引的更新」可能退化成锁全表。",[11,3894,3895],{},"记录锁锁定单条索引记录；间隙锁锁定两条记录之间的开区间；next-key lock 是记录锁加前面的间隙锁。插入时还会有插入意向锁，用来表示「准备插入这个间隙」，它与间隙锁冲突。",[11,3897,3898],{},"RC 隔离级别下基本只用记录锁；RR 下范围查询会使用 next-key lock。这也是为什么线上常用 RC：锁范围更小、死锁更少，代价是同一事务内可能出现不可重复读。",[11,3900,3901,3902,530,3904,3907,3908,3911],{},"死锁排查看 ",[45,3903,1003],{},[45,3905,3906],{},"LATEST DETECTED DEADLOCK","，MySQL 8 还可以查 ",[45,3909,3910],{},"performance_schema.data_lock_waits","。降低死锁的关键是短事务、固定加锁顺序、WHERE 条件走索引。",[136,3913,360,3915,3917],{"id":3914},"_6-explain-重点看哪些列慢-sql-怎么优化",[45,3916,3819],{}," 重点看哪些列？慢 SQL 怎么优化？",[11,3919,3920,3921,3924,3925,131,3928,131,3931,131,3934,3937,3938,3941,3942,3945,3946,3949,3950,3953,3954,364,3957,3960],{},"先看 ",[45,3922,3923],{},"type","：",[45,3926,3927],{},"const",[45,3929,3930],{},"eq_ref",[45,3932,3933],{},"ref",[45,3935,3936],{},"range"," 较好，",[45,3939,3940],{},"index"," 是全索引扫描，",[45,3943,3944],{},"ALL"," 是全表扫描。再看 ",[45,3947,3948],{},"key"," 是否命中预期索引、",[45,3951,3952],{},"key_len"," 是否符合联合索引的前缀长度、",[45,3955,3956],{},"rows",[45,3958,3959],{},"filtered"," 估算的扫描量。",[11,3962,3963,3965,3966,3968,3969,3971,3972,3975,3976,364,3979,3982],{},[45,3964,3822],{}," 信息量很大：",[45,3967,3826],{}," 是覆盖索引；",[45,3970,3857],{}," 是索引下推；",[45,3973,3974],{},"Using where"," 表示还需要在服务层过滤；",[45,3977,3978],{},"Using temporary",[45,3980,3981],{},"Using filesort"," 往往意味着排序或分组没有用上索引。",[11,3984,3985,3986,3989],{},"优化顺序通常是：确认 SQL 语义和返回量 → 补或改索引 → 重写查询（避免函数、拆 OR、减少回表）→ 调整分页和批量大小 → 最后才考虑优化器提示。MySQL 8.0.18 起可以用 ",[45,3987,3988],{},"EXPLAIN ANALYZE"," 看实际执行耗时。",[11,3991,3992,3993,3996,3997,300],{},"不要迷信 ",[45,3994,3995],{},"FORCE INDEX","，它会把优化器的判断固化成风险；统计信息不准时先 ",[45,3998,3999],{},"ANALYZE TABLE",[136,4001,4003],{"id":4002},"_7-深分页为什么慢怎么优化","7. 深分页为什么慢？怎么优化？",[11,4005,4006,4009],{},[45,4007,4008],{},"LIMIT 1000000, 20"," 需要先扫描并丢弃前 100 万行，再取 20 行，扫描量和偏移量成正比。即使命中索引，也要付出大量回表或遍历成本。",[11,4011,4012],{},"第一种方案是延迟关联：先在覆盖索引上查出主键，再关联原表取数据。子查询只扫索引，回表只发生在最终 20 行。",[1195,4014,4018],{"className":4015,"code":4016,"language":4017,"meta":613,"style":613},"language-sql shiki shiki-themes github-dark","SELECT o.*\nFROM orders o\nJOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 20) t ON o.id = t.id;\n","sql",[45,4019,4020,4025,4030],{"__ignoreMap":613},[1203,4021,4022],{"class":1205,"line":1206},[1203,4023,4024],{},"SELECT o.*\n",[1203,4026,4027],{"class":1205,"line":614},[1203,4028,4029],{},"FROM orders o\n",[1203,4031,4032],{"class":1205,"line":1217},[1203,4033,4034],{},"JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 20) t ON o.id = t.id;\n",[11,4036,4037,4038,4041],{},"第二种是游标分页：记住上一页最后一条的排序键，下一页用 ",[45,4039,4040],{},"WHERE id > ? ORDER BY id LIMIT 20","。它稳定且高效，但只能顺序翻页，不适合随机跳页。",[11,4043,4044],{},"如果业务确实需要跳页，考虑限制最大页数、改为搜索条件缩小范围，或用搜索引擎\u002F数仓承接这类查询。",[136,4046,4048],{"id":4047},"_8-大表-ddl-和分库分表要注意什么","8. 大表 DDL 和分库分表要注意什么？",[11,4050,4051],{},"MySQL 8 的 DDL 分 INSTANT、INPLACE 和 COPY 三类：加列、改默认值等在满足条件时可以秒级完成；改字符集、重建索引往往要 COPY 或 INPLACE，期间仍会消耗大量 IO。即使算法支持在线，也要先确认是否持锁、是否影响主从延迟。",[11,4053,4054],{},"执行前检查长事务和未提交事务，避免拿不到元数据锁导致后续请求排队。大表变更常用 gh-ost 或 pt-online-schema-change 做影子表切换，并安排低峰期和回滚预案。",[11,4056,4057],{},"分库分表不是第一选择：先考虑索引优化、归档冷数据、读写分离和分区表。确需拆分时，分片键要兼顾均匀性和查询命中率；跨库 join、分页、聚合和事务都会变复杂，通常要配套全局 ID、数据同步和路由层。",[11,4059,4060],{},"扩容是分库分表最容易低估的部分，提前设计好双写、迁移和校验方案，比事后补救成本低得多。",[136,4062,501],{"id":501},[503,4064,4065,4068,4071],{},[506,4066,4067],{},"「建了索引就一定走」和「走了索引就一定快」都不对，要看回表量和选择性。",[506,4069,4070],{},"RR 的「可重复读」只对快照读成立，当前读看到的是最新已提交数据。",[506,4072,4073],{},"间隙锁只在 RR 及以上、且涉及范围条件时明显，RC 下不要照搬 RR 的锁分析。",[11,4075,4076],{},"自测：① 给出一个联合索引并解释最左前缀；② 用 ReadView 的四个字段判断某个版本是否可见；③ 写出深分页的延迟关联写法并说明为什么更快。",[136,4078,577],{"id":577},[503,4080,4081,4089,4097,4111],{},[506,4082,4083,4088],{},[583,4084,4087],{"href":4085,"rel":4086},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-locking.html",[587],"MySQL 8.4 参考手册：InnoDB 锁"," — 记录锁、间隙锁与 next-key lock",[506,4090,4091,4096],{},[583,4092,4095],{"href":4093,"rel":4094},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-multi-versioning.html",[587],"MySQL 8.4 参考手册：InnoDB 多版本并发控制"," — 一致性读与隔离级别",[506,4098,4099,608,4104,131,4106,131,4108,4110],{},[583,4100,4103],{"href":4101,"rel":4102},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Fexplain-output.html",[587],"MySQL 8.4 参考手册：EXPLAIN 输出",[45,4105,3923],{},[45,4107,3948],{},[45,4109,3822],{}," 各列含义",[506,4112,4113,4118],{},[583,4114,4117],{"href":4115,"rel":4116},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-online-ddl.html",[587],"MySQL 8.4 参考手册：在线 DDL"," — INSTANT\u002FINPLACE\u002FCOPY 的边界",[1456,4120,1458],{},{"title":613,"searchDepth":614,"depth":614,"links":4122},[4123,4124,4125,4126,4127,4128,4130,4131,4132,4133],{"id":3754,"depth":614,"text":3755},{"id":3770,"depth":614,"text":3771},{"id":3812,"depth":614,"text":3813},{"id":3863,"depth":614,"text":3864},{"id":3888,"depth":614,"text":3889},{"id":3914,"depth":614,"text":4129},"6. EXPLAIN 重点看哪些列？慢 SQL 怎么优化？",{"id":4002,"depth":614,"text":4003},{"id":4047,"depth":614,"text":4048},{"id":501,"depth":614,"text":501},{"id":577,"depth":614,"text":577},"以 InnoDB 8.0 为基线，讲清聚簇索引、索引失效、MVCC 与锁，并给出 EXPLAIN、深分页和大表变更的实战方法",{},"80","\u002Fjava-interview\u002Fmysql",{"title":3671,"description":4134},{"loc":4137},"java-interview\u002Fmysql","eyt0MprGiF-WhhgLbSLh4jZ1SG2HblXh9CUpUMt30WY",{"id":4143,"title":4144,"body":4145,"category":632,"cover":633,"date":634,"description":4439,"extension":636,"group":1472,"meta":4440,"navigation":639,"order":4441,"path":4442,"seo":4443,"sitemap":4444,"stem":4445,"__hash__":4446},"javaInterview\u002Fjava-interview\u002Fnetworking.md","计算机网络：TCP、HTTP 与 Netty",{"type":8,"value":4146,"toc":4426},[4147,4150,4154,4157,4160,4167,4173,4182,4187,4195,4208,4219,4223,4226,4229,4232,4235,4239,4246,4249,4268,4271,4275,4278,4281,4284,4287,4291,4294,4297,4300,4303,4307,4322,4325,4335,4338,4342,4345,4348,4363,4366,4368,4384,4390,4392],[11,4148,4149],{},"网络题看起来零散，其实可以围绕「连接怎么建立、数据怎么可靠送达、协议怎么演进、框架怎么封装」四条线来准备。",[136,4151,4153],{"id":4152},"_1-tcp-为什么是三次握手四次挥手","1. TCP 为什么是三次握手、四次挥手？",[11,4155,4156],{},"三次握手：客户端发 SYN，服务端回 SYN-ACK，客户端再回 ACK。两次不够，是因为服务端无法确认客户端收到了自己的 SYN；同时也能防止网络中滞留的旧连接请求突然到达，造成错误连接。第三次 ACK 让双方都确认「收发能力正常、初始序列号已同步」。",[11,4158,4159],{},"四次挥手是因为 TCP 是全双工的：主动方发 FIN 只表示「我没有数据要发了」，对方还可以继续发送，所以要先回 ACK，等自己的数据发完再发 FIN。主动方收到对方的 FIN 后回 ACK，连接才完全关闭。",[11,4161,4162,4163,4166],{},"主动关闭方会进入 ",[45,4164,4165],{},"TIME_WAIT","，等待 2MSL。目的有两个：保证最后一个 ACK 能到达对方，否则对方会重发 FIN；让本连接中的旧报文在网络中消失，避免影响使用相同四元组的新连接。",[11,4168,4169,4170,4172],{},"高并发短连接的服务端容易出现大量 ",[45,4171,4165],{},"，解决方向是连接复用、长连接，而不是盲目调小等待时间。",[136,4174,190,4176,364,4178,4181],{"id":4175},"_2-time_wait-和-close_wait-分别意味着什么",[45,4177,4165],{},[45,4179,4180],{},"CLOSE_WAIT"," 分别意味着什么？",[11,4183,4184,4186],{},[45,4185,4165],{}," 出现在主动关闭方，是正常的收尾状态，通常持续 2MSL。数量很大往往说明短连接太多，或者服务端主动关闭了连接；优先考虑连接池和 keep-alive。",[11,4188,4189,4191,4192,4194],{},[45,4190,4180],{}," 出现在被动关闭方，表示「对方已经发了 FIN，但本端应用还没有调用 close」。大量 ",[45,4193,4180],{}," 几乎总是代码问题：连接没有在 finally 中关闭、异常路径漏了释放、连接池配置不当。",[11,4196,4197,4198,985,4201,4204,4205,4207],{},"排查时用 ",[45,4199,4200],{},"ss -tan state close-wait",[45,4202,4203],{},"netstat"," 看数量，再结合进程和线程栈定位是哪段代码持有连接。",[45,4206,4180],{}," 不会自己消失，必须由应用关闭。",[11,4209,4210,4211,4214,4215,4218],{},"还要区分 ",[45,4212,4213],{},"ESTABLISHED"," 过多（并发高或泄漏）和 ",[45,4216,4217],{},"SYN_RECV"," 过多（可能的 SYN Flood）。",[136,4220,4222],{"id":4221},"_3-tcp-如何保证可靠传输","3. TCP 如何保证可靠传输？",[11,4224,4225],{},"每个字节都有序号，接收方用累积确认告诉发送方「我已经收到了哪里」。超时未确认会重传；收到三个重复 ACK 时触发快速重传，不必等到超时。校验和用于发现传输错误。",[11,4227,4228],{},"滑动窗口做流量控制：接收方通过窗口字段告诉发送方还能接收多少，避免发送过快压垮接收端。窗口扩大选项让高带宽高延迟链路也能有足够吞吐。",[11,4230,4231],{},"拥塞控制则根据网络状况调整发送速率，包括慢启动、拥塞避免、快速恢复等阶段。现代内核常用 CUBIC，部分场景用 BBR，它们对丢包和延迟的假设不同。",[11,4233,4234],{},"TCP 保证的是「可靠、有序的字节流」，不保证应用层消息边界，也不保证实时性。UDP 不建立连接、不重传，适合实时音视频、DNS 这类能容忍少量丢包的场景。",[136,4236,4238],{"id":4237},"_4-粘包拆包是怎么产生的怎么解决","4. 粘包拆包是怎么产生的？怎么解决？",[11,4240,4241,4242,4245],{},"TCP 是字节流，没有消息边界。发送方两次 ",[45,4243,4244],{},"write"," 的数据可能被合并成一个包到达，也可能一条消息被拆成多个包，这就是「粘包」和「拆包」。UDP 有数据报边界，不存在这个问题，但会丢包和乱序。",[11,4247,4248],{},"解决办法是在应用层定义边界：定长消息、分隔符、或者最常用的「长度字段 + 消息体」。长度字段方案最稳妥，能处理任意二进制内容，也是大多数 RPC 协议的选择。",[11,4250,4251,4252,4255,4256,4259,4260,4263,4264,4267],{},"Netty 提供了现成的解码器：",[45,4253,4254],{},"LengthFieldBasedFrameDecoder"," 处理长度前缀，",[45,4257,4258],{},"DelimiterBasedFrameDecoder"," 处理分隔符，",[45,4261,4262],{},"FixedLengthFrameDecoder"," 处理定长。还要配合 ",[45,4265,4266],{},"IdleStateHandler"," 处理空闲连接。",[11,4269,4270],{},"注意长度字段的字节序、最大长度限制和半包缓存，恶意超长长度可能造成内存耗尽。",[136,4272,4274],{"id":4273},"_5-http11http2http3-有什么区别","5. HTTP\u002F1.1、HTTP\u002F2、HTTP\u002F3 有什么区别？",[11,4276,4277],{},"HTTP\u002F1.1 支持持久连接，但一个连接同一时刻通常只处理一个请求，浏览器靠开多个连接提升并发；管道化理论上可以连续发请求，但因队头阻塞和代理兼容问题很少使用。",[11,4279,4280],{},"HTTP\u002F2 引入二进制分帧和多路复用，一个 TCP 连接上可以并行多个流，头部用 HPACK 压缩，减少了连接数和头部开销。但 TCP 层仍有队头阻塞：一个包丢失，所有流都要等重传。",[11,4282,4283],{},"HTTP\u002F3 基于 QUIC（UDP），流之间独立，丢包只影响所在流；内置 TLS 1.3，支持 0-RTT 恢复，连接迁移也更平滑。代价是 UDP 在部分网络中被限制，运维和排查方式与 TCP 不同。",[11,4285,4286],{},"选型时还要考虑中间设备和服务端支持，HTTP\u002F2 的收益在高延迟、多资源场景最明显。",[136,4288,4290],{"id":4289},"_6-https-的握手过程是怎样的","6. HTTPS 的握手过程是怎样的？",[11,4292,4293],{},"客户端先发 ClientHello，包含支持的协议版本、加密套件和随机数；服务端回 ServerHello 和证书链。客户端验证证书：是否由受信任 CA 签发、域名是否匹配、是否在有效期内、是否被吊销。",[11,4295,4296],{},"验证通过后，双方通过密钥交换算法协商出共享密钥。现代 TLS 使用 ECDHE，具备前向安全：即使服务器私钥泄露，也无法解密过去的会话。之后用对称加密传输数据，因为对称加密性能更好。",[11,4298,4299],{},"TLS 1.3 把握手压缩到 1-RTT，并支持会话恢复时的 0-RTT，但 0-RTT 数据存在重放风险，只适合幂等请求。",[11,4301,4302],{},"中间人攻击的前提是客户端信任了攻击者的证书，所以证书校验、HSTS 和证书透明度都很重要。HTTPS 保护传输，不保护服务端被入侵后的数据。",[136,4304,4306],{"id":4305},"_7-cookiesessiontokenjwt-怎么选","7. Cookie、Session、Token、JWT 怎么选？",[11,4308,4309,4310,4313,4314,4317,4318,4321],{},"Cookie 是浏览器存储机制，",[45,4311,4312],{},"HttpOnly"," 防止脚本读取，",[45,4315,4316],{},"Secure"," 只走 HTTPS，",[45,4319,4320],{},"SameSite"," 缓解 CSRF。Session 把状态放在服务端，浏览器只存 session id，撤销方便，但需要共享存储。",[11,4323,4324],{},"Token 是广义的凭证，JWT 是其中一种自包含格式：头部、载荷、签名。服务端验签即可，不必查存储，适合水平扩展；但载荷是明文可读的，不能放敏感信息，撤销也需要额外机制。",[11,4326,4327,4328,4331,4332,4334],{},"常见组合是「短期 access token + 长期 refresh token」，敏感操作再校验权限或黑名单。把 JWT 放在 ",[45,4329,4330],{},"localStorage"," 容易受 XSS 影响，放在 ",[45,4333,4312],{}," Cookie 又要防 CSRF，需要配套措施。",[11,4336,4337],{},"选择的核心是状态放在哪、撤销要求多强、是否有跨域和跨端需求，而不是「JWT 更现代」。",[136,4339,4341],{"id":4340},"_8-netty-为什么高性能零拷贝是什么","8. Netty 为什么高性能？零拷贝是什么？",[11,4343,4344],{},"Netty 用主从 Reactor 模型：boss 线程组负责接收连接，worker 线程组负责读写事件；每个 Channel 绑定一个 EventLoop，事件在单线程内串行处理，避免锁竞争。",[11,4346,4347],{},"它基于 NIO 多路复用，支持海量连接；ByteBuf 用读指针和写指针分离读写位置，支持池化和堆外内存，减少分配与复制；Pipeline 用责任链组织编解码和业务处理，扩展方便。",[11,4349,4350,4351,4354,4355,4358,4359,4362],{},"「零拷贝」在不同语境下含义不同：操作系统层面是 ",[45,4352,4353],{},"sendfile"," 让数据不经过用户态；Netty 层面更多是 ",[45,4356,4357],{},"CompositeByteBuf"," 组合多个缓冲区、",[45,4360,4361],{},"FileRegion"," 传输文件、以及用堆外内存减少 JVM 堆与内核之间的复制。它不等于「完全不拷贝」。",[11,4364,4365],{},"使用时要防止在 EventLoop 里执行阻塞操作，否则会卡住该线程上的所有连接；还要注意 ByteBuf 的引用计数和泄漏检测，堆外内存泄漏不会体现在堆 dump 里。",[136,4367,501],{"id":501},[503,4369,4370,4373,4381],{},[506,4371,4372],{},"三次握手不是为了「确认双方在线」这么简单，核心是序列号同步和防止旧连接。",[506,4374,4375,4377,4378,4380],{},[45,4376,4165],{}," 是主动关闭方的正常状态，",[45,4379,4180],{}," 堆积多半是应用没关连接。",[506,4382,4383],{},"HTTP\u002F2 解决了 HTTP 层队头阻塞，没有解决 TCP 层；HTTP\u002F3 才换掉 TCP。",[11,4385,4386,4387,4389],{},"自测：① 解释 2MSL 的两个作用；② 说明长度字段解码器如何处理半包；③ 区分 ",[45,4388,4353],{}," 零拷贝与 Netty 的缓冲区组合。",[136,4391,577],{"id":577},[503,4393,4394,4402,4410,4418],{},[506,4395,4396,4401],{},[583,4397,4400],{"href":4398,"rel":4399},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc9293.html",[587],"RFC 9293：TCP"," — 连接管理、重传与流量控制的规范",[506,4403,4404,4409],{},[583,4405,4408],{"href":4406,"rel":4407},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc9110.html",[587],"RFC 9110：HTTP 语义"," — 方法、状态码与连接语义",[506,4411,4412,4417],{},[583,4413,4416],{"href":4414,"rel":4415},"https:\u002F\u002Fwww.rfc-editor.org\u002Frfc\u002Frfc9000.html",[587],"RFC 9000：QUIC"," — HTTP\u002F3 的传输基础",[506,4419,4420,4425],{},[583,4421,4424],{"href":4422,"rel":4423},"https:\u002F\u002Fnetty.io\u002Fwiki\u002Fuser-guide-for-4.x.html",[587],"Netty 4.x 用户指南"," — Reactor 模型、Pipeline 与编解码器",{"title":613,"searchDepth":614,"depth":614,"links":4427},[4428,4429,4431,4432,4433,4434,4435,4436,4437,4438],{"id":4152,"depth":614,"text":4153},{"id":4175,"depth":614,"text":4430},"2. TIME_WAIT 和 CLOSE_WAIT 分别意味着什么？",{"id":4221,"depth":614,"text":4222},{"id":4237,"depth":614,"text":4238},{"id":4273,"depth":614,"text":4274},{"id":4289,"depth":614,"text":4290},{"id":4305,"depth":614,"text":4306},{"id":4340,"depth":614,"text":4341},{"id":501,"depth":614,"text":501},{"id":577,"depth":614,"text":577},"从握手挥手、可靠传输到 HTTP\u002F2、HTTPS 和 Netty 的线程模型，梳理网络面试的高频考点与边界",{},"130","\u002Fjava-interview\u002Fnetworking",{"title":4144,"description":4439},{"loc":4442},"java-interview\u002Fnetworking","T3HAA_xnaKydTaGLblK09SA1O-slDi3gBNQgMLZzce8",{"id":4448,"title":4449,"body":4450,"category":632,"cover":633,"date":634,"description":4905,"extension":636,"group":1472,"meta":4906,"navigation":639,"order":4907,"path":4908,"seo":4909,"sitemap":4910,"stem":4911,"__hash__":4912},"javaInterview\u002Fjava-interview\u002Foperating-systems.md","操作系统：进程、IO 模型与容器资源",{"type":8,"value":4451,"toc":4893},[4452,4465,4550,4554,4557,4560,4563,4566,4570,4573,4576,4579,4585,4589,4592,4595,4602,4605,4609,4623,4637,4644,4657,4661,4664,4675,4681,4703,4707,4736,4747,4750,4753,4757,4760,4766,4782,4785,4789,4796,4802,4813,4816,4818,4836,4856,4858],[11,4453,4454,4455,131,4458,131,4461,4464],{},"Java 后端的操作系统题通常带着「线上」两个字：不是让你背内核源码，而是能不能用 ",[45,4456,4457],{},"top",[45,4459,4460],{},"vmstat",[45,4462,4463],{},"iostat"," 解释现象，并知道 JVM 与容器的边界。",[15,4466,4467,4480],{},[18,4468,4469],{},[21,4470,4471,4474,4477],{},[24,4472,4473],{},"现象",[24,4475,4476],{},"先看什么",[24,4478,4479],{},"常见原因",[34,4481,4482,4498,4517,4531],{},[21,4483,4484,4487,4495],{},[39,4485,4486],{},"CPU 高",[39,4488,4489,131,4491,4494],{},[45,4490,4457],{},[45,4492,4493],{},"top -Hp","、线程栈",[39,4496,4497],{},"死循环、GC、正则回溯、加密计算",[21,4499,4500,4503,4514],{},[39,4501,4502],{},"负载高但 CPU 不高",[39,4504,4505,530,4507,364,4510,4513],{},[45,4506,4460],{},[45,4508,4509],{},"r",[45,4511,4512],{},"b"," 列",[39,4515,4516],{},"IO 等待、不可中断睡眠",[21,4518,4519,4522,4528],{},[39,4520,4521],{},"内存涨",[39,4523,4524,4527],{},[45,4525,4526],{},"free","、RSS、cgroup 指标",[39,4529,4530],{},"堆外内存、page cache、线程栈",[21,4532,4533,4536,4547],{},[39,4534,4535],{},"磁盘慢",[39,4537,4538,530,4541,131,4544],{},[45,4539,4540],{},"iostat -x",[45,4542,4543],{},"await",[45,4545,4546],{},"%util",[39,4548,4549],{},"随机 IO、fsync 频繁、磁盘打满",[136,4551,4553],{"id":4552},"_1-进程线程协程有什么区别","1. 进程、线程、协程有什么区别？",[11,4555,4556],{},"进程是资源分配的基本单位，拥有独立地址空间、文件描述符和信号处理；线程是调度的基本单位，同一进程内的线程共享堆和方法区，各自有栈和寄存器。",[11,4558,4559],{},"线程切换要进入内核，保存和恢复上下文，还可能刷新 TLB、影响缓存局部性，成本通常是微秒级。进程切换还要切换地址空间，开销更大。",[11,4561,4562],{},"协程是用户态的轻量执行单元，由运行时调度，切换不经过内核，成本低。它适合大量阻塞在 IO 上的任务，但不等于「免费」：阻塞系统调用仍会占用底层线程，CPU 密集任务也不会因此变快。",[11,4564,4565],{},"Java 21 的虚拟线程由 JVM 调度到载体线程上，遇到阻塞 IO 会自动让出，本质上是把协程模型引入 JVM。注意它不能突破数据库连接池、下游限流等外部瓶颈。",[136,4567,4569],{"id":4568},"_2-虚拟内存是怎么工作的","2. 虚拟内存是怎么工作的？",[11,4571,4572],{},"每个进程看到的是独立的虚拟地址空间，通过页表映射到物理内存。CPU 访问地址时由 MMU 查表，TLB 缓存最近的映射，避免每次都走多级页表。",[11,4574,4575],{},"内存按页管理，通常 4KB。访问未映射或未加载的页会触发缺页中断：如果是合法映射但数据不在内存，就从磁盘或 swap 调入；如果是非法访问，进程收到段错误。",[11,4577,4578],{},"这种按需加载让进程可以申请超过物理内存的地址空间，也让多个进程共享同一份文件页。文件读写默认走 page cache，写操作先改缓存，变成脏页后由内核回写。",[11,4580,4581,4582,4584],{},"问题在于，",[45,4583,4526],{}," 显示的可用内存很少不一定代表内存不足，page cache 会在压力下回收。真正要关注的是可用内存、swap 使用和 OOM 事件。",[136,4586,4588],{"id":4587},"_3-五种-io-模型怎么理解epoll-为什么高效","3. 五种 IO 模型怎么理解？epoll 为什么高效？",[11,4590,4591],{},"阻塞 IO 在等待数据和拷贝数据两个阶段都阻塞；非阻塞 IO 轮询数据是否就绪，就绪后拷贝仍阻塞；IO 多路复用在单个线程里等待多个 fd 就绪；信号驱动在就绪时收到信号；异步 IO 连数据拷贝完成后再通知。",[11,4593,4594],{},"select 用位图传递 fd，有数量上限，返回后要遍历全部 fd；poll 用数组去掉了上限，但仍需遍历。epoll 把 fd 注册到内核的红黑树，就绪的 fd 放入就绪链表，调用时只返回就绪项。",[11,4596,4597,4598,4601],{},"epoll 支持水平触发和边缘触发。水平触发只要还有数据就会持续通知，编程简单；边缘触发只在状态变化时通知一次，必须一次读到 ",[45,4599,4600],{},"EAGAIN","，通常配合非阻塞 fd，效率更高但更容易写错。",[11,4603,4604],{},"Reactor 模式就是把 epoll 的事件循环与业务处理解耦，Netty、Redis、Nginx 都是这个思路。",[136,4606,4608],{"id":4607},"_4-cpu-使用率高怎么排查","4. CPU 使用率高怎么排查？",[11,4610,4611,4612,4614,4615,4618,4619,4622],{},"先用 ",[45,4613,4457],{}," 看是用户态还是内核态高：",[45,4616,4617],{},"us"," 高多半是业务计算、GC 或死循环；",[45,4620,4621],{},"sy"," 高可能是系统调用、锁竞争或上下文切换过多。",[11,4624,4625,4626,4629,4630,4632,4633,4636],{},"找到进程后，",[45,4627,4628],{},"top -Hp \u003Cpid>"," 定位到具体线程，把线程 id 转成十六进制，再在 ",[45,4631,988],{}," 输出里按 ",[45,4634,4635],{},"nid"," 搜索，就能看到对应栈。Java 进程还可以用 JFR 或 async-profiler 生成火焰图，直接看热点方法。",[11,4638,4639,4640,4643],{},"GC 也会表现为 CPU 高：用 ",[45,4641,4642],{},"jstat -gcutil"," 看回收频率和耗时，若 Full GC 频繁，问题在内存而不是计算。",[11,4645,4646,4647,530,4649,4651,4652,530,4654,4656],{},"负载高但 CPU 不高时，看 ",[45,4648,4460],{},[45,4650,4512],{}," 列和 ",[45,4653,4463],{},[45,4655,4543],{},"，可能是 IO 阻塞或大量不可中断睡眠。别只盯着 CPU 使用率一个指标。",[136,4658,4660],{"id":4659},"_5-内存不足时系统会发生什么","5. 内存不足时系统会发生什么？",[11,4662,4663],{},"当物理内存和可回收的 page cache 都不足时，内核会尝试回收、压缩，必要时使用 swap。swap 频繁换入换出会导致系统抖动，延迟急剧上升。",[11,4665,4666,4667,4670,4671,4674],{},"如果仍然无法满足，Linux 的 OOM Killer 会按评分选择进程杀掉，容器里则可能表现为 ",[45,4668,4669],{},"OOMKilled","、退出码 137。这类问题不会在 Java 堆 dump 里留下明显证据，要看 ",[45,4672,4673],{},"dmesg"," 和 cgroup 的内存事件。",[11,4676,4677,4678,4680],{},"Java 进程的内存不只是堆：还有元空间、直接内存、线程栈、JIT 代码缓存和本地库。容器内存限制下，堆要预留这些空间，不能简单地把 ",[45,4679,2656],{}," 设成容器上限。",[11,4682,4683,4684,131,4687,131,4690,131,4693,4696,4697,364,4700,300],{},"排查用 ",[45,4685,4686],{},"free -h",[45,4688,4689],{},"\u002Fproc\u002Fmeminfo",[45,4691,4692],{},"smem",[45,4694,4695],{},"jcmd VM.native_memory","，以及容器的 ",[45,4698,4699],{},"memory.current",[45,4701,4702],{},"memory.max",[136,4704,4706],{"id":4705},"_6-磁盘-io-慢怎么定位","6. 磁盘 IO 慢怎么定位？",[11,4708,3920,4709,3924,4712,131,4715,4718,4719,131,4722,4725,4726,4728,4729,4731,4732,4735],{},[45,4710,4711],{},"iostat -x 1",[45,4713,4714],{},"r\u002Fs",[45,4716,4717],{},"w\u002Fs"," 是 IOPS，",[45,4720,4721],{},"rkB\u002Fs",[45,4723,4724],{},"wkB\u002Fs"," 是吞吐，",[45,4727,4543],{}," 是平均等待时间，",[45,4730,4546],{}," 表示设备忙碌程度，",[45,4733,4734],{},"aqu-sz"," 反映排队深度。",[11,4737,4738,4740,4741,4743,4744,4746],{},[45,4739,4543],{}," 高而 ",[45,4742,4546],{}," 低，可能是单次 IO 很大或底层存储延迟高；",[45,4745,4546],{}," 接近 100% 且队列长，说明设备已经饱和。",[11,4748,4749],{},"再看是顺序还是随机：数据库和日志写入的随机 IO 对机械盘很不友好，SSD 的随机性能好得多。文件系统、RAID 和云盘的 IOPS 上限也要纳入考虑。",[11,4751,4752],{},"应用层要区分「读写量大」和「fsync 频繁」。每次写都同步刷盘会放大延迟，批量提交和合理的刷盘策略往往比换硬件更有效。",[136,4754,4756],{"id":4755},"_7-死锁的四个条件和处理方式","7. 死锁的四个条件和处理方式？",[11,4758,4759],{},"死锁需要互斥、持有并等待、不可抢占、循环等待同时成立。破坏任意一个就能避免，但工程上最常用的是固定加锁顺序和超时重试。",[11,4761,4762,4763,4765],{},"不可抢占意味着已经拿到的锁不能被强制收回，所以「按顺序申请，按逆序释放」能消除循环等待；",[45,4764,999],{}," 带超时可以让线程在拿不到锁时退出，避免无限等待。",[11,4767,4768,4769,985,4772,4774,4775,4777,4778,4781],{},"检测方面，Java 用 ",[45,4770,4771],{},"jcmd Thread.print",[45,4773,988],{},"，输出会标出相互等待的线程；数据库用 ",[45,4776,1003],{},"；操作系统层面可以看 ",[45,4779,4780],{},"\u002Fproc\u002F\u003Cpid>\u002Fstack"," 和等待链。",[11,4783,4784],{},"还要区分死锁、活锁和饥饿：活锁是线程一直在重试但无法推进，饥饿是低优先级线程长期拿不到资源。",[136,4786,4788],{"id":4787},"_8-容器里的-jvm-要注意什么","8. 容器里的 JVM 要注意什么？",[11,4790,4791,4792,4795],{},"容器用 namespace 做隔离、cgroup 做资源限制。JDK 10 起 ",[45,4793,4794],{},"UseContainerSupport"," 默认开启，JVM 会读取 cgroup 的 CPU 和内存限制，而不是宿主机总量；更早的版本会误判，必须显式设置。",[11,4797,4798,4799,4801],{},"内存方面，堆上限可以用 ",[45,4800,2660],{}," 按容器内存比例设置，但要给元空间、直接内存、线程栈和 JIT 留出空间。设置过满时，容器 OOM Killer 会直接杀掉进程，而不是抛 Java 堆 OOM。",[11,4803,4804,4805,4808,4809,4812],{},"CPU 方面，",[45,4806,4807],{},"-XX:ActiveProcessorCount"," 会影响 GC 线程数和默认并行度；",[45,4810,4811],{},"cpu.max"," 的配额可能导致 JVM 看到的核数与实际可用算力不一致，线程池要按实际配额压测。",[11,4814,4815],{},"还要注意 cgroup v1 与 v2 的指标路径不同，监控采集和 JVM 版本都要确认支持。",[136,4817,501],{"id":501},[503,4819,4820,4823,4828],{},[506,4821,4822],{},"协程不是「更快的线程」，它优化的是阻塞等待的调度成本。",[506,4824,4825,4827],{},[45,4826,4526],{}," 少不等于内存不足，page cache 可回收；容器 OOM 与 Java 堆 OOM 是两件事。",[506,4829,4830,4833,4834,300],{},[45,4831,4832],{},"epoll"," 高效的前提是 fd 已注册且事件就绪，边缘触发下必须读到 ",[45,4835,4600],{},[11,4837,4838,4839,4841,4842,4844,4845,4847,4848,131,4850,364,4853,4855],{},"自测：① 说明 ",[45,4840,4543],{}," 高但 ",[45,4843,4546],{}," 低的可能原因；② 解释容器内存限制下 ",[45,4846,2656],{}," 为什么要留余量；③ 用 ",[45,4849,4457],{},[45,4851,4852],{},"printf",[45,4854,988],{}," 串起一次 CPU 高排查。",[136,4857,577],{"id":577},[503,4859,4860,4868,4876,4887],{},[506,4861,4862,4867],{},[583,4863,4866],{"href":4864,"rel":4865},"https:\u002F\u002Fman7.org\u002Flinux\u002Fman-pages\u002Fman7\u002Fepoll.7.html",[587],"Linux man-pages：epoll"," — 水平\u002F边缘触发与就绪模型",[506,4869,4870,4875],{},[583,4871,4874],{"href":4872,"rel":4873},"https:\u002F\u002Fman7.org\u002Flinux\u002Fman-pages\u002Fman5\u002Fproc.5.html",[587],"Linux man-pages：proc"," — 进程状态、内存与线程信息",[506,4877,4878,608,4883,4886],{},[583,4879,4882],{"href":4880,"rel":4881},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F21\u002Fdocs\u002Fapi\u002Fjava.base\u002Fjava\u002Flang\u002FRuntime.html#availableProcessors()",[587],"Java SE 21：容器感知",[45,4884,4885],{},"availableProcessors"," 与容器 CPU 限制",[506,4888,4889,4892],{},[583,4890,1080],{"href":1078,"rel":4891},[587]," — 虚拟线程与平台线程的调度差异",{"title":613,"searchDepth":614,"depth":614,"links":4894},[4895,4896,4897,4898,4899,4900,4901,4902,4903,4904],{"id":4552,"depth":614,"text":4553},{"id":4568,"depth":614,"text":4569},{"id":4587,"depth":614,"text":4588},{"id":4607,"depth":614,"text":4608},{"id":4659,"depth":614,"text":4660},{"id":4705,"depth":614,"text":4706},{"id":4755,"depth":614,"text":4756},{"id":4787,"depth":614,"text":4788},{"id":501,"depth":614,"text":501},{"id":577,"depth":614,"text":577},"梳理进程线程协程、虚拟内存、epoll 与 CPU\u002F内存\u002F磁盘排查方法，并补上容器环境下 JVM 的资源边界",{},"140","\u002Fjava-interview\u002Foperating-systems",{"title":4449,"description":4905},{"loc":4908},"java-interview\u002Foperating-systems","a9B0punXurF67zwHa0wPISIavhF0-zSgCuNLz5UIGVE",{"id":4914,"title":4915,"body":4916,"category":632,"cover":633,"date":634,"description":5224,"extension":636,"group":2989,"meta":5225,"navigation":639,"order":5226,"path":5227,"seo":5228,"sitemap":5229,"stem":5230,"__hash__":5231},"javaInterview\u002Fjava-interview\u002Fredis.md","Redis：数据结构、持久化与缓存一致性",{"type":8,"value":4917,"toc":5212},[4918,4921,4925,4928,4931,4938,4941,4945,4948,4951,4974,4984,4988,4995,5014,5017,5020,5024,5027,5030,5056,5063,5067,5070,5073,5076,5079,5083,5086,5089,5092,5095,5099,5117,5120,5123,5126,5130,5144,5147,5157,5159,5173,5176,5178],[11,4919,4920],{},"Redis 题要避免停留在「有什么数据类型」。真正有区分度的是：为什么这个场景选这个结构、数据丢了最多丢多少、缓存和数据库不一致时怎么办。",[136,4922,4924],{"id":4923},"_1-redis-有哪些数据类型底层编码是什么","1. Redis 有哪些数据类型？底层编码是什么？",[11,4926,4927],{},"基础类型有 String、List、Hash、Set、ZSet，扩展类型有 Bitmap、HyperLogLog、GEO 和 Stream。",[11,4929,4930],{},"底层编码会随数据量自动切换：String 有整数编码、短字符串编码和 SDS；Redis 7 起，小 List 直接用 listpack，元素变多后转为 quicklist（由多个 listpack 组成的双向链表）；Hash 和 ZSet 在小数据量下用 listpack，超过阈值后分别转为哈希表和跳表加哈希表；Set 在纯整数时用 intset，否则用 listpack 或哈希表。",[11,4932,4933,4934,4937],{},"选型要贴合访问模式：计数器用 String 的 ",[45,4935,4936],{},"INCR","；排行榜用 ZSet；去重计数用 HyperLogLog（有误差）；签到用 Bitmap；消息队列可以用 Stream，但要评估消费组和持久化语义。",[11,4939,4940],{},"记住一个原则：结构选错，后面靠加机器补不回来。例如用 String 存整个大 JSON 再整体更新，会放大网络和序列化开销。",[136,4942,4944],{"id":4943},"_2-redis-为什么快单线程模型是怎么回事","2. Redis 为什么快？单线程模型是怎么回事？",[11,4946,4947],{},"Redis 快在几点：数据在内存；数据结构针对场景优化；用 epoll 做 IO 多路复用；命令执行单线程，避免了锁竞争和上下文切换；协议简单。",[11,4949,4950],{},"「单线程」指命令执行是单线程的。Redis 6 起引入了多线程 IO，用来处理网络读写和协议解析，命令仍在主线程串行执行，所以不需要为数据结构加锁。",[11,4952,4953,4954,4957,4958,4961,4962,4965,4966,4969,4970,4973],{},"代价是任何慢命令都会阻塞所有请求。生产上要避免 ",[45,4955,4956],{},"KEYS *","、大 key 的 ",[45,4959,4960],{},"HGETALL","、对大集合做 ",[45,4963,4964],{},"SORT","、长时间运行的 Lua 脚本，以及直接 ",[45,4967,4968],{},"DEL"," 超大 key（用 ",[45,4971,4972],{},"UNLINK"," 异步释放）。",[11,4975,4976,4977,364,4980,4983],{},"排查慢请求可以用 ",[45,4978,4979],{},"SLOWLOG GET",[45,4981,4982],{},"LATENCY DOCTOR","，并在监控里关注命令耗时和阻塞型命令的出现频率。",[136,4985,4987],{"id":4986},"_3-rdb-和-aof-怎么选会丢数据吗","3. RDB 和 AOF 怎么选？会丢数据吗？",[11,4989,4990,4991,4994],{},"RDB 是某一时刻的全量快照，",[45,4992,4993],{},"BGSAVE"," 通过 fork 子进程写盘，借助写时复制减少阻塞。文件紧凑、恢复快，但两次快照之间的数据会丢。",[11,4996,4997,4998,5001,5002,5005,5006,5009,5010,5013],{},"AOF 记录写命令，",[45,4999,5000],{},"appendfsync"," 有三个档位：",[45,5003,5004],{},"always"," 最安全但性能最差；",[45,5007,5008],{},"everysec"," 是默认值，最多丢约一秒；",[45,5011,5012],{},"no"," 交给操作系统刷盘，丢得更多。AOF 文件会持续增长，需要重写压缩。",[11,5015,5016],{},"Redis 4 起支持混合持久化：重写时先生成 RDB 格式的基线，再追加增量命令，兼顾恢复速度和数据完整度。",[11,5018,5019],{},"实际选择通常是两者都开：RDB 用于备份和快速恢复，AOF 用于缩小丢失窗口。但无论怎么配，Redis 都不是强一致存储，关键数据必须有数据库或消息队列兜底。",[136,5021,5023],{"id":5022},"_4-过期键是怎么删除的内存满了怎么办","4. 过期键是怎么删除的？内存满了怎么办？",[11,5025,5026],{},"过期删除是「惰性 + 定期」组合：访问键时检查是否过期；同时后台周期性随机抽样，把过期比例降到阈值以下。这样避免为每个键建定时器，但也意味着过期键可能短暂占内存。",[11,5028,5029],{},"从节点通常不独立删除过期键，而是等待主节点的删除命令同步，保证主从数据一致。",[11,5031,5032,5033,5036,5037,5040,5041,790,5044,5047,5048,5051,5052,5055],{},"内存达到 ",[45,5034,5035],{},"maxmemory"," 后按淘汰策略处理：",[45,5038,5039],{},"noeviction"," 直接报错；",[45,5042,5043],{},"allkeys-lru",[45,5045,5046],{},"allkeys-lfu"," 在所有键里淘汰；",[45,5049,5050],{},"volatile-*"," 只淘汰设置了过期时间的键；",[45,5053,5054],{},"volatile-ttl"," 优先淘汰剩余时间短的。",[11,5057,5058,5059,5062],{},"LRU 是近似实现，靠采样若干键选最久未用的，",[45,5060,5061],{},"maxmemory-samples"," 越大越精确。LFU 更适合「曾经热、现在冷」的访问分布，但计数器需要衰减。",[136,5064,5066],{"id":5065},"_5-缓存穿透击穿雪崩分别怎么解决","5. 缓存穿透、击穿、雪崩分别怎么解决？",[11,5068,5069],{},"穿透是查不存在的数据，请求每次都打到数据库。可以用布隆过滤器提前拦截，或缓存空值并设置较短过期时间；接口层还要做参数校验和限流。",[11,5071,5072],{},"击穿是某个热点 key 过期瞬间，大量请求同时回源。常见做法是互斥重建（只放一个请求查库，其他等待）或逻辑过期（值不过期，异步刷新），前者简单但会阻塞，后者体验好但会读到旧值。",[11,5074,5075],{},"雪崩是大量 key 同时失效，或 Redis 整体不可用。解决思路包括给过期时间加随机抖动、做多级缓存、集群化部署、加熔断降级和限流，以及准备本地兜底数据。",[11,5077,5078],{},"三者容易混：穿透是「数据不存在」，击穿是「单个热点失效」，雪崩是「大面积同时失效或整体故障」。",[136,5080,5082],{"id":5081},"_6-如何保证缓存和数据库的一致性","6. 如何保证缓存和数据库的一致性？",[11,5084,5085],{},"常用的是旁路缓存：读时先查缓存，未命中查库并回填；写时先更新数据库，再删除缓存。",[11,5087,5088],{},"为什么是删除而不是更新缓存：并发更新时，两个写请求的缓存更新顺序可能与数据库提交顺序相反，留下脏数据；删除让下一次读自然回填。",[11,5090,5091],{},"即使先更新库再删缓存，仍有极小概率不一致：读请求在删除前拿到旧值，删除后又把旧值写回。可以用延迟双删、给缓存加版本号，或订阅 binlog 异步删除来收敛。",[11,5093,5094],{},"要接受「最终一致」而不是「强一致」。对一致性要求极高的场景，应让缓存只承载可容忍短暂陈旧的数据，或在读路径上做版本校验。",[136,5096,5098],{"id":5097},"_7-redis-分布式锁怎么做有哪些坑","7. Redis 分布式锁怎么做？有哪些坑？",[11,5100,5101,5102,3924,5105,5108,5109,5112,5113,5116],{},"基本写法是 ",[45,5103,5104],{},"SET lock_key unique_value NX PX 30000",[45,5106,5107],{},"NX"," 保证互斥，",[45,5110,5111],{},"PX"," 设置过期防止死锁，",[45,5114,5115],{},"unique_value"," 标识持有者。",[11,5118,5119],{},"释放时必须用 Lua 先比较 value 再删除，避免误删别人的锁；业务执行时间可能超过锁租期，需要看门狗续期，Redisson 就封装了这套逻辑。",[11,5121,5122],{},"仍有两个本质问题：主从异步复制下，主节点加锁成功后宕机，锁可能丢失；锁过期与业务完成之间存在时间竞态。前者需要接受「Redis 锁不是强一致」或改用共识系统，后者可以用 fencing token 让下游拒绝旧持有者的写。",[11,5124,5125],{},"所以 Redis 锁适合「防重复、降并发」这类场景，不适合转账扣款这种必须绝对互斥的核心链路。Redlock 也有广泛争议，不要把「多数节点加锁成功」等同于强一致。",[136,5127,5129],{"id":5128},"_8-主从哨兵cluster-有什么区别热-key-和大-key-怎么处理","8. 主从、哨兵、Cluster 有什么区别？热 key 和大 key 怎么处理？",[11,5131,5132,5133,5136,5137,790,5140,5143],{},"主从解决读扩展和备份，复制是异步的，主节点故障需要人工切换。哨兵在主从之上做监控和自动故障转移，靠多数派投票选主，仍是一个主节点写。Cluster 做分片，16384 个槽分布在多个主节点上，客户端按 ",[45,5134,5135],{},"CRC16(key) mod 16384"," 路由，遇到 ",[45,5138,5139],{},"MOVED",[45,5141,5142],{},"ASK"," 重定向；多 key 操作要求落在同一槽，可用 hash tag 强制同槽。",[11,5145,5146],{},"热 key 指单个 key 的访问量过大，会打满某个节点。可以做本地缓存、把 key 拆成多个副本分散读、增加只读副本，或在代理层做请求合并。",[11,5148,5149,5150,5152,5153,5156],{},"大 key 指 value 过大或集合元素过多，会造成网络阻塞、删除卡顿和迁移困难。应拆分结构、分页读取、用 ",[45,5151,4972],{}," 删除，并通过 ",[45,5154,5155],{},"redis-cli --bigkeys"," 和慢日志定期发现。",[136,5158,501],{"id":501},[503,5160,5161,5164,5170],{},[506,5162,5163],{},"单线程指的是命令执行，不是整个进程只有一个线程。",[506,5165,5166,5167,5169],{},"AOF ",[45,5168,5008],{}," 仍可能丢约一秒，Redis 不能替代持久化数据库。",[506,5171,5172],{},"缓存一致性只能做到最终一致，方案要配套业务容忍度和监控。",[11,5174,5175],{},"自测：① 说明 ZSet 在小数据量和大数据量下的编码差异；② 比较互斥重建与逻辑过期在击穿场景的优劣；③ 写出分布式锁的加锁与释放命令，并解释 value 为什么必须唯一。",[136,5177,577],{"id":577},[503,5179,5180,5188,5196,5204],{},[506,5181,5182,5187],{},[583,5183,5186],{"href":5184,"rel":5185},"https:\u002F\u002Fredis.io\u002Fdocs\u002Flatest\u002Fdevelop\u002Fdata-types\u002F",[587],"Redis 官方文档：数据类型"," — 各类型的语义与典型场景",[506,5189,5190,5195],{},[583,5191,5194],{"href":5192,"rel":5193},"https:\u002F\u002Fredis.io\u002Fdocs\u002Flatest\u002Foperate\u002Foss_and_stack\u002Fmanagement\u002Fpersistence\u002F",[587],"Redis 官方文档：持久化"," — RDB、AOF 与混合持久化",[506,5197,5198,5203],{},[583,5199,5202],{"href":5200,"rel":5201},"https:\u002F\u002Fredis.io\u002Fdocs\u002Flatest\u002Fdevelop\u002Fuse\u002Fpatterns\u002Fdistributed-locks\u002F",[587],"Redis 官方文档：分布式锁"," — 单实例锁与 Redlock 的说明",[506,5205,5206,5211],{},[583,5207,5210],{"href":5208,"rel":5209},"https:\u002F\u002Fredis.io\u002Fdocs\u002Flatest\u002Fdevelop\u002Fclients\u002Fclient-side-caching\u002F",[587],"Redis 官方文档：客户端缓存"," — 多级缓存与失效通知",{"title":613,"searchDepth":614,"depth":614,"links":5213},[5214,5215,5216,5217,5218,5219,5220,5221,5222,5223],{"id":4923,"depth":614,"text":4924},{"id":4943,"depth":614,"text":4944},{"id":4986,"depth":614,"text":4987},{"id":5022,"depth":614,"text":5023},{"id":5065,"depth":614,"text":5066},{"id":5081,"depth":614,"text":5082},{"id":5097,"depth":614,"text":5098},{"id":5128,"depth":614,"text":5129},{"id":501,"depth":614,"text":501},{"id":577,"depth":614,"text":577},"从底层编码、单线程模型到 RDB\u002FAOF、淘汰策略和分布式锁，系统梳理 Redis 面试题与线上故障的应对方式",{},"90","\u002Fjava-interview\u002Fredis",{"title":4915,"description":5224},{"loc":5227},"java-interview\u002Fredis","yHXdZtk_Aojf-8OglrYewWd--oWMETFu30fKxA9HXrM",{"id":5233,"title":5234,"body":5235,"category":632,"cover":633,"date":634,"description":5613,"extension":636,"group":2989,"meta":5614,"navigation":639,"order":5615,"path":5616,"seo":5617,"sitemap":5618,"stem":5619,"__hash__":5620},"javaInterview\u002Fjava-interview\u002Fspring.md","Spring：Bean 生命周期、AOP 与事务",{"type":8,"value":5236,"toc":5599},[5237,5240,5249,5252,5264,5267,5271,5274,5303,5316,5327,5331,5334,5337,5351,5355,5358,5367,5377,5381,5394,5401,5412,5419,5437,5452,5455,5459,5480,5483,5494,5498,5512,5525,5540,5542,5553,5564,5566],[11,5238,5239],{},"Spring 题有一条很自然的链路：容器怎么创建 Bean、怎么给 Bean 加能力、事务为什么依赖代理、Boot 又怎么把这一切自动化。按这条线准备，比孤立背注解有效。以下默认 Spring Framework 6 \u002F Spring Boot 3。",[136,5241,5243,5244,571,5246,421],{"id":5242},"_1-怎么理解-ioc-和-dibeanfactory-与-applicationcontext-有什么区别","1. 怎么理解 IoC 和 DI？",[45,5245,1168],{},[45,5247,5248],{},"ApplicationContext",[11,5250,5251],{},"IoC 是控制反转：对象的创建、依赖装配和生命周期交给容器，业务代码只声明需要什么。DI 是它的实现方式，通过构造器、setter 或字段注入把依赖传进来。",[11,5253,5254,5256,5257,5260,5261,5263],{},[45,5255,1168],{}," 是最基础的容器接口，提供 ",[45,5258,5259],{},"getBean"," 和 Bean 生命周期管理，默认懒加载。",[45,5262,5248],{}," 继承它，额外提供资源加载、事件发布、国际化、AOP 集成和注解支持，单例默认在启动时实例化。",[11,5265,5266],{},"实践上优先构造器注入：依赖不可变、便于单元测试、能暴露循环依赖问题。字段注入写法简单，但会让类难以脱离容器测试，也掩盖了职责过多的信号。",[136,5268,5270],{"id":5269},"_2-bean-的生命周期是怎样的","2. Bean 的生命周期是怎样的？",[11,5272,5273],{},"大致分四段：实例化、属性填充、初始化、销毁。",[11,5275,5276,5277,5280,5281,131,5284,5287,5288,5291,5292,131,5295,5298,5299,5302],{},"实例化通过构造器或工厂方法完成；随后注入依赖并回调各类 ",[45,5278,5279],{},"Aware"," 接口，例如 ",[45,5282,5283],{},"BeanNameAware",[45,5285,5286],{},"ApplicationContextAware","。接着执行 ",[45,5289,5290],{},"BeanPostProcessor"," 的前置处理，再调用 ",[45,5293,5294],{},"@PostConstruct",[45,5296,5297],{},"InitializingBean.afterPropertiesSet"," 和自定义 ",[45,5300,5301],{},"init-method","，最后执行后置处理——AOP 代理通常就在这一步生成。",[11,5304,5305,5306,131,5309,364,5312,5315],{},"使用阶段结束后，容器关闭时依次调用 ",[45,5307,5308],{},"@PreDestroy",[45,5310,5311],{},"DisposableBean.destroy",[45,5313,5314],{},"destroy-method","。注意原型 Bean 的销毁方法不会由容器调用，需要调用方自己负责。",[11,5317,5318,5320,5321,131,5324,5326],{},[45,5319,5290],{}," 是理解 Spring 扩展性的关键：它能在初始化前后改写甚至替换 Bean，",[45,5322,5323],{},"@Autowired",[45,5325,1380],{},"、事务代理都依赖这一类扩展点。",[136,5328,5330],{"id":5329},"_3-循环依赖是怎么解决的为什么构造器注入不行","3. 循环依赖是怎么解决的？为什么构造器注入不行？",[11,5332,5333],{},"单例 Bean 的字段或 setter 循环依赖，靠三级缓存解决：一级放成品 Bean，二级放提前暴露的早期引用，三级放能生成早期引用的工厂。",[11,5335,5336],{},"创建 A 时先实例化并放入三级缓存，填充属性发现依赖 B，转去创建 B；B 依赖 A 时从缓存拿到 A 的早期引用，B 完成后 A 继续填充并最终完成。三级缓存的意义在于：如果 A 需要被 AOP 代理，工厂能在暴露早期引用时就生成代理，而不是暴露原始对象。",[11,5338,5339,5340,5343,5344,131,5347,5350],{},"构造器注入要求在实例化阶段就拿到依赖，此时对象还不存在，无法提前暴露，因此会抛 ",[45,5341,5342],{},"BeanCurrentlyInCreationException","。Spring Boot 2.6 起默认禁止循环依赖，需要显式开启才恢复旧行为；更好的做法是重构依赖或使用 ",[45,5345,5346],{},"@Lazy",[45,5348,5349],{},"ObjectProvider"," 延迟获取。",[136,5352,5354],{"id":5353},"_4-bean-是线程安全的吗单例要注意什么","4. Bean 是线程安全的吗？单例要注意什么？",[11,5356,5357],{},"Spring 不保证 Bean 线程安全，它只负责创建和管理。单例 Bean 被所有请求共享，只要存在可变的成员状态，就可能出现并发问题。",[11,5359,5360,5361,5363,5364,5366],{},"无状态的 Service、DAO 天然安全；有状态的对象要么改成方法内局部变量，要么用 ",[45,5362,934],{},"（并记得清理），要么把作用域改成原型。给单例加 ",[45,5365,693],{}," 通常只是把瓶颈集中到一个点上。",[11,5368,5369,5370,5373,5374,5376],{},"常见误区是把 ",[45,5371,5372],{},"@Scope(\"prototype\")"," 注到单例里就以为每次调用都会新建：注入只发生一次，单例持有的是同一个原型实例。需要每次获取新对象时应注入 ",[45,5375,5349],{}," 或使用作用域代理。",[136,5378,5380],{"id":5379},"_5-spring-aop-的原理是什么为什么自调用会失效","5. Spring AOP 的原理是什么？为什么自调用会失效？",[11,5382,5383,5384,5387,5388,5390,5391,5393],{},"Spring AOP 基于动态代理：目标类实现了接口时可用 JDK 代理，否则用 CGLIB 生成子类。Spring Boot 默认 ",[45,5385,5386],{},"proxyTargetClass = true","，即优先 CGLIB，所以 ",[45,5389,2137],{}," 类和 ",[45,5392,2137],{}," 方法无法被代理。",[11,5395,5396,5397,5400],{},"调用方拿到的是代理对象，代理在方法前后织入通知。所谓自调用失效，是指同类中 ",[45,5398,5399],{},"this.otherMethod()"," 绕过了代理，通知自然不会执行；事务、缓存、异步注解都会遇到这个问题。",[11,5402,5403,5404,5407,5408,5411],{},"解决方式：拆到另一个 Bean、注入自身代理、使用 ",[45,5405,5406],{},"AopContext.currentProxy()","（需开启 ",[45,5409,5410],{},"exposeProxy","），或者改用 AspectJ 编译期\u002F加载期织入。生产上更推荐前两种，侵入性小。",[136,5413,360,5415,5418],{"id":5414},"_6-transactional-在哪些情况下会失效传播行为怎么选",[45,5416,5417],{},"@Transactional"," 在哪些情况下会失效？传播行为怎么选？",[11,5420,5421,5422,5425,5426,5429,5430,364,5433,5436],{},"失效的常见原因：自调用绕过代理；方法不是 ",[45,5423,5424],{},"public","（Spring 6 起类代理支持 protected 和包可见方法，但仍有边界）；Bean 不是由容器管理；异常被 ",[45,5427,5428],{},"catch"," 后没有抛出；抛出的是受检异常而默认只回滚 ",[45,5431,5432],{},"RuntimeException",[45,5434,5435],{},"Error","；数据库表本身不支持事务；在另一个线程里执行。",[11,5438,5439,5440,5443,5444,5447,5448,5451],{},"传播行为中，",[45,5441,5442],{},"REQUIRED"," 是默认值，加入现有事务；",[45,5445,5446],{},"REQUIRES_NEW"," 挂起当前事务、独立提交，适合记日志等「必须落库」的操作；",[45,5449,5450],{},"NESTED"," 使用保存点，只对 JDBC 等支持保存点的资源有效。",[11,5453,5454],{},"还要注意只读事务和超时设置，以及事务内不要做远程调用——长事务会长期占用连接和锁。",[136,5456,5458],{"id":5457},"_7-spring-boot-自动配置的原理","7. Spring Boot 自动配置的原理？",[11,5460,5461,5464,5465,5468,5469,5472,5473,5476,5477,300],{},[45,5462,5463],{},"@SpringBootApplication"," 组合了配置类、组件扫描和 ",[45,5466,5467],{},"@EnableAutoConfiguration","。后者通过 ",[45,5470,5471],{},"AutoConfigurationImportSelector"," 读取候选配置类：Boot 2.7 起使用 ",[45,5474,5475],{},"META-INF\u002Fspring\u002Forg.springframework.boot.autoconfigure.AutoConfiguration.imports","，更早的版本用 ",[45,5478,5479],{},"spring.factories",[11,5481,5482],{},"候选类并不会全部生效，而是由条件注解筛选：类路径上是否存在某个类、容器里是否已有同类型 Bean、配置项是否开启。这就是「引入 starter 就自动装配，同时允许自定义覆盖」的机制。",[11,5484,5485,5486,5489,5490,5493],{},"自定义覆盖之所以生效，是因为自动配置类通常带 ",[45,5487,5488],{},"@ConditionalOnMissingBean","，且用户配置先于自动配置处理。排查自动配置问题时，可以开启 ",[45,5491,5492],{},"debug=true"," 查看条件评估报告。",[136,5495,5497],{"id":5496},"_8-一个-http-请求在-spring-mvc-里是怎么走完的","8. 一个 HTTP 请求在 Spring MVC 里是怎么走完的？",[11,5499,5500,5501,5504,5505,5508,5509,300],{},"请求先经过 Servlet 容器的过滤器链，进入 ",[45,5502,5503],{},"DispatcherServlet","。它向 ",[45,5506,5507],{},"HandlerMapping"," 询问由哪个处理器处理，拿到处理器和拦截器链后，交给匹配的 ",[45,5510,5511],{},"HandlerAdapter",[11,5513,5514,5516,5517,5520,5521,5524],{},[45,5515,5511],{}," 调用参数解析器把请求参数、路径变量、请求体转换成方法参数，执行 Controller 方法；返回值再由返回值处理器处理，",[45,5518,5519],{},"@ResponseBody"," 走 ",[45,5522,5523],{},"HttpMessageConverter"," 序列化，否则交给视图解析器渲染。",[11,5526,5527,5528,5531,5532,5535,5536,5539],{},"拦截器的 ",[45,5529,5530],{},"preHandle"," 在处理器执行前调用，",[45,5533,5534],{},"postHandle"," 在渲染前调用，",[45,5537,5538],{},"afterCompletion"," 在完成后调用。过滤器属于 Servlet 规范，能拿到原始请求但不能感知处理器；拦截器属于 Spring MVC，能访问处理器信息。",[136,5541,501],{"id":501},[503,5543,5544,5547,5550],{},[506,5545,5546],{},"三级缓存只解决单例的字段\u002Fsetter 循环依赖，不解决构造器注入和原型作用域。",[506,5548,5549],{},"AOP 的一切「注解不生效」几乎都可以归结为「没走代理」。",[506,5551,5552],{},"自动配置不是「无条件装配」，条件注解和加载顺序才是重点。",[11,5554,5555,5556,5558,5559,571,5561,5563],{},"自测：① 画出 Bean 从实例化到销毁的关键回调顺序；② 说出三种让 ",[45,5557,5417],{}," 恢复生效的改法；③ 解释 ",[45,5560,5446],{},[45,5562,5450],{}," 在回滚行为上的差异。",[136,5565,577],{"id":577},[503,5567,5568,5576,5584,5592],{},[506,5569,5570,5575],{},[583,5571,5574],{"href":5572,"rel":5573},"https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Freference\u002Fcore\u002Fbeans.html",[587],"Spring Framework：IoC 容器与 Bean 生命周期"," — 容器、作用域与扩展点",[506,5577,5578,5583],{},[583,5579,5582],{"href":5580,"rel":5581},"https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Freference\u002Fdata-access\u002Ftransaction.html",[587],"Spring Framework：事务管理"," — 传播行为与回滚规则",[506,5585,5586,5591],{},[583,5587,5590],{"href":5588,"rel":5589},"https:\u002F\u002Fdocs.spring.io\u002Fspring-boot\u002Freference\u002Fusing\u002Fauto-configuration.html",[587],"Spring Boot：自动配置"," — 条件注解与加载顺序",[506,5593,5594,5598],{},[583,5595,5597],{"href":1443,"rel":5596},[587],"Spring Framework：AOP 编程"," — 代理机制与自调用问题",{"title":613,"searchDepth":614,"depth":614,"links":5600},[5601,5603,5604,5605,5606,5607,5609,5610,5611,5612],{"id":5242,"depth":614,"text":5602},"1. 怎么理解 IoC 和 DI？BeanFactory 与 ApplicationContext 有什么区别？",{"id":5269,"depth":614,"text":5270},{"id":5329,"depth":614,"text":5330},{"id":5353,"depth":614,"text":5354},{"id":5379,"depth":614,"text":5380},{"id":5414,"depth":614,"text":5608},"6. @Transactional 在哪些情况下会失效？传播行为怎么选？",{"id":5457,"depth":614,"text":5458},{"id":5496,"depth":614,"text":5497},{"id":501,"depth":614,"text":501},{"id":577,"depth":614,"text":577},"从 IoC 容器、三级缓存到 AOP 代理和事务失效，梳理 Spring 与 Spring Boot 的高频面试链路",{},"60","\u002Fjava-interview\u002Fspring",{"title":5234,"description":5613},{"loc":5616},"java-interview\u002Fspring","AmlsYLEuEzrSk7bwJoj1_y8H6mOO3Py3-eIjBxU1XJE",{"id":5622,"title":5623,"body":5624,"category":632,"cover":633,"date":634,"description":5868,"extension":636,"group":1472,"meta":5869,"navigation":639,"order":5870,"path":5871,"seo":5872,"sitemap":5873,"stem":5874,"__hash__":5875},"javaInterview\u002Fjava-interview\u002Ftroubleshooting.md","线上排障：CPU、内存与依赖故障",{"type":8,"value":5625,"toc":5856},[5626,5629,5633,5647,5661,5664,5667,5671,5674,5684,5691,5697,5701,5704,5707,5710,5713,5717,5726,5729,5732,5738,5742,5745,5748,5751,5754,5758,5761,5764,5771,5774,5778,5781,5784,5787,5790,5794,5797,5800,5803,5806,5808,5819,5822,5824],[11,5627,5628],{},"排障题考的是「有没有一套稳定的动作」，而不是背工具名。通用顺序是：先确认影响面和最近变更，再用指标缩小范围，然后取证，最后止血与复盘。下面所有命令都应在测试环境先验证，生产执行前评估影响。",[136,5630,5632],{"id":5631},"_1-线上-cpu-飙高怎么排查","1. 线上 CPU 飙高怎么排查？",[11,5634,5635,5636,5638,5639,131,5641,131,5643,5646],{},"先看整体：",[45,5637,4457],{}," 确认是哪个进程，观察 ",[45,5640,4617],{},[45,5642,4621],{},[45,5644,5645],{},"wa"," 哪个高。用户态高通常是业务计算或 GC；系统态高可能是系统调用、锁竞争或上下文切换。",[11,5648,5649,5650,5652,5653,5656,5657,5660],{},"再定位线程：",[45,5651,4628],{}," 找到最耗 CPU 的线程，把十进制 id 转成十六进制，在 ",[45,5654,5655],{},"jstack \u003Cpid>"," 里按 ",[45,5658,5659],{},"nid=0x..."," 搜索。连续抓两三次，看它是否停在同一个方法。",[11,5662,5663],{},"业务代码的常见原因：死循环、正则灾难性回溯、超大集合排序、频繁 JSON 序列化、日志拼接、加密计算。如果是 GC 线程占用高，转去看 GC 日志和堆使用。",[11,5665,5666],{},"止血手段包括限流、回滚最近发布、摘除异常实例；不要直接重启了事，先保留线程栈和堆信息，否则根因就丢了。",[136,5668,5670],{"id":5669},"_2-内存持续上涨怎么定位","2. 内存持续上涨怎么定位？",[11,5672,5673],{},"先区分堆内和堆外。看 GC 后老年代是否回落：如果每次 Full GC 后仍然上涨，怀疑泄漏；如果 GC 后能回落但很快又涨，可能是流量或缓存策略问题。",[11,5675,5676,5677,5680,5681,5683],{},"堆内取证用 ",[45,5678,5679],{},"jcmd \u003Cpid> GC.heap_dump"," 导出，配合 ",[45,5682,2614],{}," 自动保存；用 MAT 看支配树、对象数量和 GC Root 路径，重点找「本该释放却被集合或线程持有」的对象。",[11,5685,5686,5687,5690],{},"堆外要看直接内存（NIO、Netty）、元空间（动态生成类）、线程栈和 JIT 代码缓存。",[45,5688,5689],{},"jcmd VM.native_memory summary"," 需要启动时开启 NMT，Netty 可以开泄漏检测。",[11,5692,5693,5694,5696],{},"常见根因：本地缓存无上限、",[45,5695,934],{}," 未清理、监听器只增不减、批量查询一次加载过多数据。修复前先确认对象的生命周期，不要用「调大堆」掩盖泄漏。",[136,5698,5700],{"id":5699},"_3-接口突然变慢排查顺序是什么","3. 接口突然变慢，排查顺序是什么？",[11,5702,5703],{},"先看影响范围：是所有接口还是某个接口，是单实例还是全部实例，是否与发布、配置变更、流量高峰同时发生。这一步能排除大半方向。",[11,5705,5706],{},"再看链路：用 traceId 找到耗时最长的那一跳。如果时间花在数据库，看慢查询、锁等待和连接池；花在远程调用，看下游 RT、超时和重试；花在应用内，看线程池队列、锁竞争和 GC 停顿。",[11,5708,5709],{},"注意平均值会骗人，要看 P95、P99 和错误率。重试和超时配置不当会让下游变慢放大成雪崩，表现为「所有服务一起慢」。",[11,5711,5712],{},"还要检查资源：CPU 是否打满、连接数是否耗尽、磁盘是否打满、网络是否有丢包。定位后用限流、扩容、降级或回滚止血，再做根因修复。",[136,5714,5716],{"id":5715},"_4-频繁-full-gc-怎么处理","4. 频繁 Full GC 怎么处理？",[11,5718,5719,5720,985,5723,300],{},"先确认频率和原因：用 GC 日志看每次回收前后的占用、停顿时间、晋升量，以及是否伴随 ",[45,5721,5722],{},"GC overhead limit exceeded",[45,5724,5725],{},"promotion failed",[11,5727,5728],{},"如果是内存泄漏，堆 dump 会显示老年代对象持续增长，必须修代码；如果是堆太小或大对象过多，评估扩容和对象生命周期；如果是元空间不足，检查动态代理、脚本引擎和热部署。",[11,5730,5731],{},"G1 下还要关注 Humongous 对象：超过 Region 一半的大对象会占用连续 Region，频繁创建大数组会触发并发周期甚至 Full GC。",[11,5733,5734,5735,5737],{},"处理顺序是：先止血（扩容、限流、回滚），再取证（GC 日志、堆 dump），最后修复。不要只调 ",[45,5736,2656],{},"，那只是把问题推迟。",[136,5739,5741],{"id":5740},"_5-线程池打满有什么表现怎么止损","5. 线程池打满有什么表现？怎么止损？",[11,5743,5744],{},"表现通常是接口 RT 上升、超时增多、队列长度持续增长、拒绝数上升，严重时上游开始熔断。异步任务则表现为「提交成功但很久不执行」。",[11,5746,5747],{},"先看监控：活跃线程数、队列长度、完成任务数、拒绝次数、任务耗时分布。再看线程栈，确认线程卡在数据库、远程调用还是锁上。",[11,5749,5750],{},"止血可以临时扩容、提高消费能力、把非核心任务摘出去，或者对入口限流。根因通常是下游变慢、任务无界提交、线程池之间互相阻塞，或者同步调用被放进了异步池。",[11,5752,5753],{},"长期要做池隔离（核心与非核心分开）、有界队列、明确拒绝策略、按下游容量设计并发，并给队列等待时间设上限。",[136,5755,5757],{"id":5756},"_6-数据库连接池耗尽怎么办","6. 数据库连接池耗尽怎么办？",[11,5759,5760],{},"先看连接池指标：活跃连接、空闲连接、等待线程数、获取连接超时次数。活跃连接长期占满且不释放，通常是长事务或连接泄漏。",[11,5762,5763],{},"长事务的常见来源：事务里做远程调用、批量处理过多数据、手动开启事务后异常路径没提交\u002F回滚。连接泄漏则是借出后没有在 finally 中归还。",[11,5765,5766,5767,5770],{},"用数据库侧的证据交叉验证：",[45,5768,5769],{},"SHOW PROCESSLIST"," 看连接状态和执行的 SQL，慢查询日志找长时间运行的语句，锁等待也会让连接一直占用。",[11,5772,5773],{},"止血可以回滚、杀掉异常会话、临时扩容，但根本解决要缩短事务、修复泄漏、优化慢 SQL。连接池不是越大越好，超过数据库承载能力只会让争用更严重。",[136,5775,5777],{"id":5776},"_7-redis-或-mq-故障时怎么降级","7. Redis 或 MQ 故障时怎么降级？",[11,5779,5780],{},"先明确依赖等级：哪些功能可以短时间不可用，哪些必须保底。缓存故障时，可以用本地缓存兜底、限流保护数据库、对非核心读请求返回默认值；同时防止「缓存失效后全部打到数据库」的雪崩。",[11,5782,5783],{},"消息队列故障时，生产端可以降级为同步调用、写入本地消息表稍后补发，或暂时拒绝非核心消息；消费端要保证重复消费幂等，恢复后按积压量平滑追平，避免瞬间打满下游。",[11,5785,5786],{},"降级开关要提前做好并演练，包含触发条件、影响范围、恢复步骤和责任人。不要等故障时才临时改代码。",[11,5788,5789],{},"所有降级都要有监控和告警：降级本身是「有损可用」，必须让团队知道当前处于降级状态。",[136,5791,5793],{"id":5792},"_8-发布出问题怎么回滚如何减少这类事故","8. 发布出问题怎么回滚？如何减少这类事故？",[11,5795,5796],{},"先判断能否快速回滚：应用版本回滚通常最快；数据库变更往往不可逆，所以要做「扩展-收缩」式变更，先加字段、双写、再切读、最后删旧字段，保证新旧版本兼容。",[11,5798,5799],{},"发布策略上，灰度或金丝雀发布先放少量流量，观察错误率、延迟和关键业务指标，再逐步扩大。功能开关可以把「发布」和「上线」分开，出问题时关开关比回滚更快。",[11,5801,5802],{},"回滚前要保留现场：日志、指标、trace、异常堆栈。回滚后确认数据一致性，尤其是消息、缓存和异步任务是否留下中间状态。",[11,5804,5805],{},"事后复盘关注可行动的改进：补充监控、加自动化检查、缩小变更批次、完善回滚脚本。把「这次是谁的错」换成「哪个环节让错误没被更早发现」。",[136,5807,501],{"id":501},[503,5809,5810,5813,5816],{},[506,5811,5812],{},"排障先止血再根因，但止血前尽量保留线程栈、堆 dump、GC 日志等现场。",[506,5814,5815],{},"堆内存上涨不等于堆泄漏，直接内存、元空间和线程栈要单独看。",[506,5817,5818],{},"扩连接池、扩线程池常常只是把压力转移到更下游。",[11,5820,5821],{},"自测：① 给出一次 CPU 高的完整命令链；② 说明如何区分内存泄漏和容量不足；③ 设计一个缓存故障时的三级降级方案。",[136,5823,577],{"id":577},[503,5825,5826,5832,5840,5848],{},[506,5827,5828,5831],{},[583,5829,2721],{"href":2719,"rel":5830},[587]," — 线程栈、堆信息、堆转储与 JFR",[506,5833,5834,5839],{},[583,5835,5838],{"href":5836,"rel":5837},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F21\u002Ftroubleshoot\u002F",[587],"Java SE 21 故障排查指南"," — OOM、GC 与原生内存问题",[506,5841,5842,5847],{},[583,5843,5846],{"href":5844,"rel":5845},"https:\u002F\u002Fsre.google\u002Fsre-book\u002Fmanaging-incidents\u002F",[587],"Google SRE Book：应急响应"," — 事件分级、止血与复盘",[506,5849,5850,5855],{},[583,5851,5854],{"href":5852,"rel":5853},"https:\u002F\u002Fgithub.com\u002Fbrettwooldridge\u002FHikariCP",[587],"HikariCP 配置说明"," — 连接池容量与超时参数",{"title":613,"searchDepth":614,"depth":614,"links":5857},[5858,5859,5860,5861,5862,5863,5864,5865,5866,5867],{"id":5631,"depth":614,"text":5632},{"id":5669,"depth":614,"text":5670},{"id":5699,"depth":614,"text":5700},{"id":5715,"depth":614,"text":5716},{"id":5740,"depth":614,"text":5741},{"id":5756,"depth":614,"text":5757},{"id":5776,"depth":614,"text":5777},{"id":5792,"depth":614,"text":5793},{"id":501,"depth":614,"text":501},{"id":577,"depth":614,"text":577},"用一套可复用的排查顺序处理 CPU 飙高、内存上涨、接口变慢、Full GC、线程池与连接池耗尽及发布回滚",{},"160","\u002Fjava-interview\u002Ftroubleshooting",{"title":5623,"description":5868},{"loc":5871},"java-interview\u002Ftroubleshooting","snEyVpoV3RgdiGAcIubOnCYjxNAiDLd86_rM8oGo59k",[5877,5880,5884,5888,5892,5896,5900,5904,5908,5912,5916,5920,5923,5927,5931,5935,5939,5943,5947,5951,5955,5959,5963,5966,5970,5974,5978,5982,5986,5990,5994,5998,6002,6006,6009,6013,6017,6021,6025,6029,6033,6037,6041,6045,6049,6052,6056,6060,6064,6068,6072,6076,6080,6084,6088,6092,6095,6099,6103,6107,6111,6115,6119,6123,6127,6131,6135,6138,6142,6146,6150,6154,6158,6162,6166,6170,6174,6178,6181,6185,6189,6193,6197,6201,6205,6209,6213,6217,6221,6224,6228,6232,6236,6240,6244,6248,6252,6256,6260,6264,6267,6271,6275,6279,6283,6287,6291,6295,6299,6303,6307,6310,6314,6318,6322,6326,6330,6334,6338,6342,6346,6350,6353,6357,6361,6365,6369,6373,6377,6381,6385,6389,6393,6396,6400,6404,6408,6412,6416,6420,6424,6428,6432,6436,6439,6443,6447,6451,6455,6459,6463,6467,6471,6475,6479,6482,6486,6490,6494,6498,6502,6506,6510,6514,6518,6522,6525,6529,6533,6537,6541,6545,6549,6553,6557,6561],{"id":641,"title":6,"titles":5878,"content":5879,"level":1206},[],"对比 List、Map、Set 与阻塞队列的实现差异，讲清 fail-fast、CopyOnWrite 和不可变集合的适用边界 集合题的失分点通常不是「不知道 API」，而是选型理由说不清、边界条件记混。本文把常用实现放在同一张表里对照，再逐题拆开。 需求 首选 避免 随机访问、尾部追加 ArrayList 头部频繁插入用 LinkedList 栈、队列、双端队列 ArrayDeque 用 LinkedList 充当队列 键值查找，无需排序 HashMap 多线程共享时用 HashMap 保留插入顺序 LinkedHashMap 为排序而用 LinkedHashMap 按 key 排序 \u002F 范围查询 TreeMap 大数据量下用 TreeMap 做计数 高并发读写 ConcurrentHashMap Hashtable、Collections.synchronizedMap",{"id":5881,"title":617,"titles":5882,"content":5883,"level":614},"\u002Fjava-interview\u002Fcollections#_1-arraylistlinkedlistarraydeque-怎么选",[6],"ArrayList 基于连续数组，随机访问 O(1)，尾部追加摊还 O(1)，中间插入要移动元素；LinkedList 是双向链表，头尾操作 O(1)，但按下标访问是 O(n)，而且每个节点都有额外的对象头和前后指针，缓存局部性差。 实践结论：绝大多数场景选 ArrayList。即使频繁在头部插入，ArrayDeque 的环形数组通常也比 LinkedList 快；只有在持有节点引用做大量中间删除时，链表才有优势。 ArrayDeque 不允许 null，容量按 2 倍增长，作为栈比 Stack 更合适，因为 Stack 继承自 Vector，每个方法都加锁。",{"id":5885,"title":619,"titles":5886,"content":5887,"level":614},"\u002Fjava-interview\u002Fcollections#_2-arraylist-的扩容过程是怎样的",[6],"默认构造出来的是空数组，第一次 add 才扩到 10。之后容量不够时按 oldCapacity + (oldCapacity >> 1) 扩到约 1.5 倍，再用 Arrays.copyOf 复制到新数组。 扩容是 O(n) 的复制，所以能预估大小时应该用 new ArrayList\u003C>(size) 一次性分配，避免多次搬移。trimToSize() 可以回收多余容量，但会再复制一次，不要频繁调用。 注意 subList() 返回的是视图而非拷贝，原列表结构修改后视图会抛 ConcurrentModificationException；Arrays.asList() 返回定长列表，add 会抛异常，但 set 可以改。",{"id":5889,"title":621,"titles":5890,"content":5891,"level":614},"\u002Fjava-interview\u002Fcollections#_3-hashmaplinkedhashmaptreemapconcurrenthashmap-怎么选",[6],"HashMap 用哈希表，平均 O(1)，不保证顺序；LinkedHashMap 在哈希表基础上加了双向链表，可保留插入顺序或访问顺序，适合做 LRU；TreeMap 基于红黑树，O(log n)，支持排序和范围查询，但 key 必须可比较。 ConcurrentHashMap 用 CAS 加桶级锁保证并发安全，读操作基本无锁，适合多线程共享的缓存和计数场景。它不允许 null 键值，这一点和 HashMap 不同。 选择顺序通常是：先问要不要并发，再问要不要顺序，最后才看单次操作复杂度。很多「用 TreeMap 排序」的需求，其实在查询时对结果排一次序更划算。",{"id":5893,"title":282,"titles":5894,"content":5895,"level":614},"\u002Fjava-interview\u002Fcollections#_4-什么是-fail-fast遍历时删除为什么会抛异常",[6],"ArrayList、HashMap 等非并发集合在结构修改时会改变 modCount；迭代器创建时记下期望值，每次 next() 都检查两者是否一致，不一致就抛 ConcurrentModificationException。 所以 for (String s : list) list.remove(s) 会失败，正确写法是 iterator.remove()，或者用 removeIf，后者内部同样走了迭代器。单线程下这是主动检测；多线程下它只是「尽力而为」，不能当并发保护使用。 ConcurrentHashMap 的迭代器是弱一致的：不抛异常，但不保证看到遍历开始后的所有修改。CopyOnWriteArrayList 的迭代器基于快照，同样不抛异常，也看不到快照之后的更新。",{"id":5897,"title":624,"titles":5898,"content":5899,"level":614},"\u002Fjava-interview\u002Fcollections#_5-copyonwritearraylist-适合什么场景代价是什么",[6],"写操作时复制整个数组，在副本上修改，再用 ReentrantLock 保护发布；读操作完全无锁，直接读当前数组引用。 它适合「读多写极少、且读方不能接受加锁」的场景，例如监听器列表、黑白名单、路由配置。写操作是 O(n) 复制，元素多或写频繁时性能会很差，内存峰值也会翻倍。 它的迭代器是快照式的：遍历期间看不到新写入，也不会抛 ConcurrentModificationException；iterator.remove() 不支持，要删元素得直接调列表的 remove。如果要求实时一致，应该换 ConcurrentHashMap 对应的并发结构或加锁。",{"id":5901,"title":626,"titles":5902,"content":5903,"level":614},"\u002Fjava-interview\u002Fcollections#_6-comparable-和-comparator-有什么区别排序要注意什么",[6],"Comparable 由元素自身实现 compareTo，定义「自然顺序」，一个类通常只有一种；Comparator 是外部比较器，可以按需定义多种排序规则，配合 Comparator.comparing(...).thenComparing(...) 组合。 注意三点：比较器必须满足自反、反对称、传递，否则 TimSort 可能抛 IllegalArgumentException: Comparison method violates its general contract；用减法实现比较可能整数溢出，应该用 Integer.compare；null 值要显式处理。 Collections.sort 和 Arrays.sort 对对象数组使用稳定的归并类算法，相等元素的相对顺序会保留；对基本类型数组则使用不稳定但更快的双轴快排。",{"id":5905,"title":628,"titles":5906,"content":5907,"level":614},"\u002Fjava-interview\u002Fcollections#_7-listof-和-collectionsunmodifiablelist-有什么区别",[6],"List.of() 创建的是真正不可变且独立的列表：内部拷贝了元素，之后原数组或原集合的修改不会影响它，任何 add、set、remove 都抛异常，且不允许 null 元素。 Collections.unmodifiableList() 是包装视图：它自己拒绝修改，但底层列表变化时会「透传」出来，因此只读保证是相对的。适合「对外暴露只读视图、内部仍需修改」的场景。 Arrays.asList() 又是另一种：返回定长视图，set 可以，add\u002Fremove 不行，且与原数组共享数据。三者不要混用。",{"id":5909,"title":459,"titles":5910,"content":5911,"level":614},"\u002Fjava-interview\u002Fcollections#_8-并发场景下常用哪些阻塞队列怎么选",[6],"ArrayBlockingQueue 有界、数组实现、单把锁配两个条件队列，适合容量可控的生产消费；LinkedBlockingQueue 默认近似无界（Integer.MAX_VALUE），用两把锁分离入队出队，吞吐更高但必须显式设容量，否则堆积时容易 OOM。 SynchronousQueue 不存储元素，生产者必须等到消费者接手，适合直接交接；PriorityBlockingQueue 无界、按优先级出队；DelayQueue 用于定时任务和缓存过期；LinkedTransferQueue 在「生产者等待消费者」的场景下效率更高。 线程池选队列要结合拒绝策略一起看：用无界队列时 maximumPoolSize 基本不会生效，任务会一直排队，这是很多线上事故的根源。",{"id":5913,"title":501,"titles":5914,"content":5915,"level":614},"\u002Fjava-interview\u002Fcollections#易错点与自测",[6],"subList、Arrays.asList、unmodifiableList 都是视图，只有 List.of 和 new ArrayList\u003C>(...) 是独立拷贝。 「线程安全」不等于「复合操作原子」：ConcurrentHashMap 的 get 后再 put 仍需 computeIfAbsent 或显式锁。 HashMap 允许一个 null 键和多个 null 值，ConcurrentHashMap 全都不允许。 自测：① 说明为什么 ArrayDeque 比 LinkedList 更适合做队列；② 写出安全删除 ArrayList 元素的三种方式；③ 解释 List.of 与 unmodifiableList 在数据同步上的差异。",{"id":5917,"title":577,"titles":5918,"content":5919,"level":614},"\u002Fjava-interview\u002Fcollections#参考资料",[6],"java.util 包 API（Java 21） — 各集合实现的复杂度与线程安全声明 ArrayList 源码（OpenJDK） — 扩容与 modCount 检查 JEP 269：Convenience Factory Methods for Collections — List.of 的不可变语义",{"id":1101,"title":648,"titles":5921,"content":5922,"level":1206},[],"围绕可见性、原子性、有序性梳理 volatile、CAS、AQS、线程池和 ThreadLocal，补上虚拟线程的适用边界 并发题最忌讳只背名词。建议先建立一条主线：多个线程访问共享状态时，需要同时处理可见性、原子性和有序性，volatile、锁、CAS、线程池都是围绕这三件事展开的工具。 工具 解决什么 不解决什么 volatile 可见性、禁止重排 复合操作原子性 synchronized 互斥、可见性、有序性 跨进程、超时中断 CAS 单个变量的原子更新 多变量事务、ABA 线程池 线程复用、限流、治理 任务内部的线程安全",{"id":5924,"title":725,"titles":5925,"content":5926,"level":614},"\u002Fjava-interview\u002Fconcurrency#_1-创建线程有哪几种方式为什么推荐线程池",[648],"本质上只有一种：构造 Thread 并调用 start()。所谓多种方式是任务的定义形式不同：继承 Thread、实现 Runnable、实现 Callable 配合 FutureTask、提交到线程池，JDK 21 还可以用虚拟线程执行器。 推荐线程池的原因：线程创建和销毁有系统调用与栈分配开销；无限制创建会耗尽内存和调度资源；池化后可以统一控制并发度、队列、拒绝策略、命名和监控。 直接用 new Thread 的代码通常缺少统一的异常处理和关闭逻辑。execute 抛出的异常会交给线程的未捕获异常处理器，submit 则会把它封装进 Future，不调用 get 就永远看不到——这是线上「任务静默失败」的常见原因。",{"id":5928,"title":1086,"titles":5929,"content":5930,"level":614},"\u002Fjava-interview\u002Fconcurrency#_2-synchronized-的实现原理是什么锁升级是怎么回事",[648],"synchronized 编译成 monitorenter \u002F monitorexit 字节码，修饰方法时通过方法访问标志隐式加锁。锁信息记录在对象头的 Mark Word 中。 传统讲解分为偏向锁、轻量级锁、重量级锁：无竞争时偏向某个线程；出现竞争时用 CAS 尝试把 Mark Word 指向栈上的锁记录；竞争加剧则膨胀为操作系统互斥量，未抢到的线程进入等待队列。 需要补充版本边界：偏向锁从 JDK 15 起默认关闭并被标记废弃，后续版本已移除，现代 JDK 不应再把它当作必然存在的优化。锁升级在常规理解中不可逆，但具体实现有安全点等细节，面试不必展开。",{"id":5932,"title":1088,"titles":5933,"content":5934,"level":614},"\u002Fjava-interview\u002Fconcurrency#_3-volatile-能保证什么不能保证什么",[648],"它保证可见性：写操作会刷新到主内存，读操作会从主内存重新读取；同时禁止特定重排序，建立 happens-before 关系。它不保证复合操作的原子性，count++ 依然是读、加、写三步。 典型正确用法：状态标志位、一次性安全发布（双重检查锁单例的实例字段）、作为 CAS 的底层语义基础。典型错误用法：用 volatile 修饰计数器，或用它保护「先检查再修改」的逻辑。 双重检查锁里必须给实例字段加 volatile，否则可能先写入引用、后完成构造，其他线程会看到一个「半初始化」对象。",{"id":5936,"title":829,"titles":5937,"content":5938,"level":614},"\u002Fjava-interview\u002Fconcurrency#_4-cas-是什么aba-问题怎么解决",[648],"CAS 包含三个值：内存位置、期望值、新值，仅当当前值等于期望值时才更新，由 CPU 指令保证原子性。Java 通过 Unsafe 的本地方法暴露，AtomicInteger 等类封装了它。 它避免了加锁的阻塞和上下文切换，但有几个问题：自旋失败会空耗 CPU；只能保证单个变量的原子性；存在 ABA——值从 A 改成 B 又改回 A，CAS 无法察觉中间变化。 ABA 的解决思路是加版本号，AtomicStampedReference 就是「值 + 版本」；如果对象引用变化本身无害，也可以忽略。高并发计数场景可以用 LongAdder，它把热点分散到多个 Cell，代价是求和结果不是某一瞬间的精确快照。",{"id":5940,"title":1091,"titles":5941,"content":5942,"level":614},"\u002Fjava-interview\u002Fconcurrency#_5-aqs-的原理是什么reentrantlock-和-synchronized-怎么选",[648],"AQS 用一个 volatile int state 表示同步状态，加一个 FIFO 的双向等待队列。获取失败的线程被包装成节点入队并挂起，释放时唤醒后继节点；子类只需实现 tryAcquire \u002F tryRelease 等模板方法。 ReentrantLock 用 state 记录重入次数，Semaphore 用它表示许可数，CountDownLatch 用它表示剩余计数。公平锁在获取时先检查队列中是否有前驱，非公平锁允许新线程直接抢，吞吐通常更高。 选型上：需要可中断获取、超时、公平性或多个条件队列时用 ReentrantLock；否则优先 synchronized，它由 JVM 优化、写法简单、不会忘记释放。虚拟线程场景下还要注意，JDK 21 的 synchronized 会钉住载体线程。",{"id":5944,"title":905,"titles":5945,"content":5946,"level":614},"\u002Fjava-interview\u002Fconcurrency#_6-线程池的核心参数和执行流程线程数怎么设",[648],"ThreadPoolExecutor 有七个参数：核心线程数、最大线程数、空闲存活时间、时间单位、工作队列、线程工厂、拒绝策略。 提交任务时：核心线程未满就新建线程；核心满了进队列；队列满了且未达最大线程数就继续建线程；再满则执行拒绝策略。所以无界队列会让最大线程数永远不生效。 线程数没有万能公式。CPU 密集任务通常取核数或核数加一；IO 密集可用「核数 ×（1 + 等待时间 \u002F 计算时间）」估算，再结合实际压测。比公式更重要的是给队列设上界、给线程命名、监控活跃数与队列长度。 拒绝策略默认是 AbortPolicy，直接抛异常。 CallerRunsPolicy 让提交线程执行，能形成天然反压，但会阻塞上游，需要确认业务能接受。",{"id":5948,"title":1094,"titles":5949,"content":5950,"level":614},"\u002Fjava-interview\u002Fconcurrency#_7-threadlocal-的原理和内存泄漏原因",[648],"每个 Thread 内部有一个 ThreadLocalMap，key 是 ThreadLocal 的弱引用，value 是强引用。线程复用（尤其是线程池）时，map 会跟着线程长期存活。 泄漏的根源不是 key 的弱引用本身，而是「key 被回收后，value 仍被 Entry 强引用，而 Entry 又被线程持有」。虽然 get、set 会顺带清理部分过期条目，但不保证及时。 规范做法是在 finally 中调用 remove()。InheritableThreadLocal 只在创建线程时拷贝，线程池复用会失效；跨线程池传递上下文需要 TransmittableThreadLocal 这类方案。虚拟线程数量极大，更要注意 ThreadLocal 的内存占用。",{"id":5952,"title":978,"titles":5953,"content":5954,"level":614},"\u002Fjava-interview\u002Fconcurrency#_8-如何排查死锁虚拟线程又解决了什么问题",[648],"死锁需要同时满足互斥、持有并等待、不可抢占、循环等待四个条件。排查时先用 jcmd \u003Cpid> Thread.print -l 或 jstack 抓线程栈，JDK 会直接输出 Found one Java-level deadlock 以及相互等待的锁和代码行。 预防手段：统一加锁顺序、用 tryLock 带超时、缩小临界区、避免在持锁时调用外部服务。数据库死锁则是另一套机制，要看 SHOW ENGINE INNODB STATUS。 虚拟线程解决的是「大量阻塞任务导致平台线程耗尽」的问题：阻塞时自动让出载体线程，调度成本低，适合高并发 IO。它不提升单核计算能力，也不能突破下游连接池、数据库连接数的瓶颈；用信号量限制并发往往比池化虚拟线程更合适。",{"id":5956,"title":501,"titles":5957,"content":5958,"level":614},"\u002Fjava-interview\u002Fconcurrency#易错点与自测",[648],"volatile 不等于原子，synchronized 不等于公平，ConcurrentHashMap 不等于复合操作原子。 Executors 的快捷工厂方法隐藏了无界队列和无上限线程数，生产环境应显式构造 ThreadPoolExecutor。 ThreadLocal 必须 remove，线程池与虚拟线程会放大忘记清理的后果。 自测：① 写出双重检查锁单例并解释 volatile 的作用；② 说明线程池在「核心满、队列满、未到最大线程数」时的行为；③ 用 jcmd 命令列出排查死锁的操作。",{"id":5960,"title":577,"titles":5961,"content":5962,"level":614},"\u002Fjava-interview\u002Fconcurrency#参考资料",[648],"JLS SE 21 第 17 章：线程与锁 — happens-before 与内存模型的规范定义 ThreadPoolExecutor API — 参数语义、执行流程与拒绝策略 JEP 374：Deprecate and Disable Biased Locking — 偏向锁的版本边界 JEP 444：Virtual Threads — 虚拟线程的调度模型与限制",{"id":1475,"title":1108,"titles":5964,"content":5965,"level":1206},[],"用 SOLID 原则串起单例、工厂、策略、模板方法、代理和责任链，说明它们在 Spring 中的真实落点与过度设计边界 设计模式题最怕两种回答：一种是背定义，另一种是把所有模式都往项目里塞。好的回答会说明「这个变化点在哪里、引入模式后谁不需要改」。",{"id":5967,"title":1117,"titles":5968,"content":5969,"level":614},"\u002Fjava-interview\u002Fdesign-patterns#_1-solid-原则怎么落到代码里",[1108],"单一职责：一个类只对一类变化负责。判断标准不是「代码行数」，而是「改一个需求时，是否总在改同一个类」。 开闭原则：对扩展开放、对修改关闭。实践上靠接口、策略注册和依赖注入，而不是靠预测所有未来需求。 里氏替换：子类必须能在不破坏语义的前提下替换父类。典型反例是正方形继承长方形、或子类重写方法后抛出父类没有的异常。 接口隔离：接口要小而专，避免实现类被迫实现用不到的方法。依赖倒置：高层模块依赖抽象，Spring 的注入就是它的落地。 此外还有迪米特法则（少暴露内部结构）、组合优于继承。原则之间会冲突，真正的能力是在具体场景里取舍。",{"id":5971,"title":1136,"titles":5972,"content":5973,"level":614},"\u002Fjava-interview\u002Fdesign-patterns#_2-单例有几种写法哪种最好",[1108],"饿汉式在类加载时就创建，简单且线程安全，但会提前占用资源；懒汉式延迟创建，朴素的加锁实现性能差。 双重检查锁需要 volatile 修饰实例，否则可能读到「引用已赋值但构造未完成」的对象。静态内部类利用类加载机制实现懒加载和线程安全，写法最简洁。 枚举单例天然防止反射和反序列化破坏，是《Effective Java》推荐的方式；如果不需要延迟加载，它是最稳的选择。 要注意：Spring 的单例是「容器内单例」，不是 JVM 全局单例；一个类可以被多个容器各创建一个实例。分布式场景下，单例也只在本进程内有效。",{"id":5975,"title":1155,"titles":5976,"content":5977,"level":614},"\u002Fjava-interview\u002Fdesign-patterns#_3-工厂模式解决了什么问题",[1108],"简单工厂用一个方法按参数创建对象，把 new 集中起来；工厂方法把创建延迟到子类；抽象工厂用于创建一组相关产品。 它们的共同价值是：调用方只依赖抽象，不关心具体类型和构造细节；新增产品时尽量不改调用方。Spring 的 BeanFactory 就是工厂思想，配合配置和依赖注入，把对象创建与使用解耦。 但工厂不是越多越好。对象只有一个实现、构造也简单时，直接 new 更清楚。工厂真正有价值，是在创建逻辑复杂（需要选择实现、组装依赖、缓存实例）或需要统一生命周期时。 如果工厂里出现大量 if\u002Felse 分支，往往说明可以用注册表加策略来替代。",{"id":5979,"title":1186,"titles":5980,"content":5981,"level":614},"\u002Fjava-interview\u002Fdesign-patterns#_4-策略模式怎么消除-if-else边界在哪",[1108],"先抽象出策略接口，每个分支实现一个策略；再用 Map\u003CString, Strategy> 注册，调用时按类型取策略。注册可以手写，也可以让 Spring 注入所有实现并按标记建索引。 \u002F**\n * 按支付渠道选择策略并执行支付。\n *\n * @param channel 支付渠道标识\n * @param amount  支付金额，单位分\n * @return 支付结果\n *\u002F\npublic PayResult pay(String channel, long amount) {\n    PayStrategy strategy = strategies.get(channel);\n    if (strategy == null) {\n        throw new IllegalArgumentException(\"不支持的渠道: \" + channel);\n    }\n    return strategy.pay(amount);\n} 边界在于：如果分支逻辑很短、变化不频繁，保留 switch 反而更易读。策略模式增加的是类数量和间接层，只有当每个分支都有独立复杂度、需要独立测试或扩展时才划算。 还要避免「策略只包一层空壳」：如果所有实现都在调同一个方法，只是参数不同，可能用配置更合适。",{"id":5983,"title":1300,"titles":5984,"content":5985,"level":614},"\u002Fjava-interview\u002Fdesign-patterns#_5-模板方法和回调有什么区别",[1108],"模板方法在父类定义算法骨架，把可变步骤留给子类实现，属于继承式扩展。JdbcTemplate、AbstractList、Servlet 的 service 都是这个思路。 回调把一段行为作为参数传入，由框架在合适时机调用，属于组合式扩展。Java 里用函数式接口、匿名类或 Lambda 表达，比如 JdbcTemplate.query 的 RowMapper。 区别在耦合方式：模板方法要求继承，父类改动会影响所有子类；回调更灵活，可以复用同一个方法，也便于测试。 选择时优先组合：如果变化点只有一个或少数几个，回调足够；如果骨架固定且步骤较多，模板方法更清晰。Spring 的很多「模板」类其实同时提供了回调和子类扩展两种方式。",{"id":5987,"title":1333,"titles":5988,"content":5989,"level":614},"\u002Fjava-interview\u002Fdesign-patterns#_6-代理装饰适配器怎么区分",[1108],"三者都通过包装对象改变行为，但目的不同。 代理控制访问：调用方和被代理对象实现同一接口，代理在调用前后加逻辑（鉴权、事务、远程调用）。Spring AOP 就是代理。 装饰增强功能：同样实现同一接口，但重点是叠加职责，可以层层嵌套，比如 BufferedInputStream 包 FileInputStream。 适配器转换接口：把一个接口转成调用方期望的另一个接口，解决不兼容问题，比如 InputStreamReader 把字节流转成字符流。 判断方法：看包装后接口是否变化、调用方是否需要感知。接口不变且为了控制访问是代理，为了增强是装饰，接口变了是适配器。",{"id":5991,"title":1363,"titles":5992,"content":5993,"level":614},"\u002Fjava-interview\u002Fdesign-patterns#_7-观察者模式在-spring-里怎么用",[1108],"Spring 的事件机制就是观察者模式：ApplicationEventPublisher 发布事件，@EventListener 标注的方法接收事件，容器负责找到监听器。 默认是同步调用，发布者会等所有监听器执行完；可以用 @Async 改异步，但要配置线程池，并注意异常不会传回发布者。@TransactionalEventListener 可以把监听时机绑定到事务提交后，避免「事务还没提交，监听器已经查库」。 风险包括：监听器抛异常影响主流程（同步时）、异步丢失顺序、循环发布导致死循环、监听器过多难以追踪。事件适合「一对多、弱依赖」的通知，不适合需要强一致返回结果的流程。",{"id":5995,"title":1392,"titles":5996,"content":5997,"level":614},"\u002Fjava-interview\u002Fdesign-patterns#_8-责任链模式适合什么场景",[1108],"责任链把请求沿处理器链传递，每个处理器决定处理、继续还是中断。Servlet 的 Filter、Spring MVC 的 Interceptor、Netty 的 Pipeline 都是它的实现。 它的价值是解耦：新增一个校验或日志处理器，不需要改调用方；顺序和组合可以通过配置调整。 代价是链路不直观，出问题时需要知道「谁处理了、谁短路了」。要明确每个处理器的职责和顺序，并提供日志或追踪。 不要为了「看起来灵活」把简单流程拆成一堆处理器。两三个步骤的校验，用顺序代码更清楚；只有当处理器数量会增长、顺序需要动态调整时，责任链才值得。",{"id":5999,"title":501,"titles":6000,"content":6001,"level":614},"\u002Fjava-interview\u002Fdesign-patterns#易错点与自测",[1108],"设计原则是权衡工具，不是越抽象越好；模式引入的间接层要有明确收益。 单例的线程安全取决于是否有可变状态，容器单例不等于全局单例。 Spring 事件默认同步，异常处理和事务边界要显式设计。 自测：① 用策略模式改写一段三层的 if\u002Felse，并说明什么时候不该改；② 区分代理和装饰的意图差异；③ 解释 @TransactionalEventListener 解决什么问题。",{"id":6003,"title":577,"titles":6004,"content":6005,"level":614},"\u002Fjava-interview\u002Fdesign-patterns#参考资料",[1108],"Spring Framework：事件与监听器 — 同步、异步与事务事件 Spring Framework：AOP 与代理 — 代理模式的实际应用 Refactoring.Guru：设计模式目录 — 各模式的意图与结构对照 html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":1699,"title":1482,"titles":6007,"content":6008,"level":1206},[],"讲清 CAP\u002FBASE 的真实含义、分布式 ID 与幂等设计，并梳理分库分表、一致性哈希和高可用的工程权衡 分布式题很容易变成名词接龙。建议抓住一条判断标准：在故障和并发下，系统愿意牺牲什么、又能保证到什么程度。下面每个问题都尽量落到这个取舍上。",{"id":6010,"title":1491,"titles":6011,"content":6012,"level":614},"\u002Fjava-interview\u002Fdistributed-systems#_1-cap-和-base-到底在说什么",[1482],"C 是一致性，通常指线性一致：读到的必须是最新已完成的写；A 是可用性，每个请求都能在有限时间内得到非错误响应；P 是分区容错，网络分区时系统仍能继续工作。 关键点是：网络分区不是「要不要容忍」，而是迟早会发生。所以真实选择是「分区发生时保 C 还是保 A」，而不是平时三选二。很多回答说「我们选了 CA」，在跨机房部署下基本不成立。 BASE 是对 AP 的工程化补充：基本可用、软状态、最终一致。它不是放弃正确性，而是把一致性从「时刻」放宽到「一段时间内」。 还有一个常被追问的 PACELC：分区时在 C 和 A 之间选，没有分区时在延迟和一致性之间选。多数系统平时也要为一致性付出延迟。",{"id":6014,"title":1507,"titles":6015,"content":6016,"level":614},"\u002Fjava-interview\u002Fdistributed-systems#_2-分布式-id-有哪些方案雪花算法的坑在哪",[1482],"UUID 本地生成、无依赖，但无序且较长，做主键会造成 B+ 树页分裂和索引膨胀。数据库自增简单可靠，但单点、扩展难，分库后需要步长或号段。 雪花算法生成 64 位 ID：1 位符号、41 位毫秒时间、10 位机器标识、12 位序列。趋势递增、性能高，但强依赖时钟，回拨可能产生重复 ID；机器标识的分配也需要统一管理。 常见的回拨处理：小幅回拨等待追平，大幅回拨直接报错或切换备用 workerId；也可以用「上次时间戳 + 序列」的本地状态兜底。 号段模式（如 Leaf）从数据库批量取一段 ID 在内存分配，减少数据库压力，但要处理号段耗尽时的预取和容灾。最终选型看是否需要趋势递增、能否接受中心依赖，以及前端是否会被 JS 精度截断。",{"id":6018,"title":1523,"titles":6019,"content":6020,"level":614},"\u002Fjava-interview\u002Fdistributed-systems#_3-如何设计一个幂等接口",[1482],"幂等的本质是「同一个业务意图执行多次，结果与执行一次相同」。先确定幂等键：客户端生成的请求号、业务单号，或「用户 + 操作 + 目标」的组合。 常见实现：数据库唯一索引（最可靠，但要注意捕获重复键异常）；去重表或 Redis SET NX 记录已处理键（要设置合理 TTL）；状态机限制非法迁移；乐观锁版本号；Token 机制（先领 token，提交时消费）。 还要处理并发窗口：两个相同请求同时到达，都查到「未处理」，然后都执行。唯一约束或分布式锁能把窗口关掉，单纯「先查再写」不行。 设计时明确三点：幂等键由谁生成、保存多久、重复请求返回什么（成功结果、进行中还是冲突）。这些约定比具体技术更重要。",{"id":6022,"title":1543,"titles":6023,"content":6024,"level":614},"\u002Fjava-interview\u002Fdistributed-systems#_4-分布式锁有哪些实现怎么选",[1482],"Redis 锁性能高、实现简单，SET NX PX 加 Lua 释放，配合续期；但主从异步复制下存在锁丢失窗口，适合防重复而非强互斥。 ZooKeeper 用临时顺序节点：创建最小序号节点者获得锁，其他节点监听前一个节点。会话断开后临时节点自动删除，可靠性较好；缺点是性能低于 Redis，且依赖 ZK 集群。 数据库锁可以用唯一索引或 SELECT ... FOR UPDATE，实现最简单，但性能和扩展性最差，适合低频、强一致的场景。 选择时看失败代价：重复发一条通知，Redis 锁足够；扣减余额，应该用数据库行锁、乐观锁或共识系统，并配合 fencing token 防止旧持有者继续写。",{"id":6026,"title":1567,"titles":6027,"content":6028,"level":614},"\u002Fjava-interview\u002Fdistributed-systems#_5-为什么要分库分表会带来哪些问题",[1482],"单表数据量大时，索引变深、DDL 变慢、备份恢复困难；单库连接数和写入能力也有上限。分表解决数据量，分库解决连接数和写扩展。 拆分方式有垂直和水平：垂直按业务或字段拆，边界清晰但可能留下大表；水平按分片键拆，扩展性好，但跨片查询变难。 问题清单要能说出来：跨库 join、跨片分页和聚合、分布式事务、全局唯一 ID、跨片唯一约束、路由规则变更、数据迁移与扩容、热点分片。 工程上通常配合中间件（如 ShardingSphere）做路由和归并，但中间件不能消灭跨片成本。设计分片键时优先选查询必带、分布均匀的字段；如果业务必须按两个维度查，考虑冗余表或搜索引擎。",{"id":6030,"title":1583,"titles":6031,"content":6032,"level":614},"\u002Fjava-interview\u002Fdistributed-systems#_6-一致性哈希解决了什么问题",[1482],"普通取模 hash(key) % N 在节点数变化时几乎全部 key 都要重新映射，缓存会大面积失效。一致性哈希把节点和 key 映射到同一个环上，key 顺时针找到第一个节点，增删节点只影响相邻区间。 为了解决节点分布不均，引入了虚拟节点：每个物理节点在环上放置多个副本，key 的分布更均匀，节点扩容时迁移也更平滑。 它仍有两个边界：一是热点 key 不会因为哈希而分散，需要额外拆分；二是节点性能不同，等权虚拟节点可能造成负载不均，要按权重调整。 一致性哈希常用于缓存和对象存储，数据库分片更常用范围或哈希取模，因为要兼顾范围查询和运维可控性。",{"id":6034,"title":1603,"titles":6035,"content":6036,"level":614},"\u002Fjava-interview\u002Fdistributed-systems#_7-zookeeper-的典型用法和原理",[1482],"ZooKeeper 是一个高可用的协调服务，数据组织成树形 znode，支持持久节点、临时节点和顺序节点。它通过 ZAB 协议在 leader 和 follower 之间复制日志，写请求由 leader 处理，多数派确认后提交。 临时节点随会话消失，因此常用于服务注册和故障感知；顺序节点配合 watch 可以实现公平锁；节点数据小、读多写少，适合存元数据和配置，不适合存大对象或高频写。 watch 是一次性的：触发后需要重新注册，客户端要处理「注册前发生变化」的窗口。会话超时时间决定了故障感知的快慢，设得太短容易误判，太长则恢复慢。 同类系统 etcd 基于 Raft，提供租约、MVCC 和更现代的 API；选型时看生态和团队经验，不要只看「谁更快」。",{"id":6038,"title":1619,"titles":6039,"content":6040,"level":614},"\u002Fjava-interview\u002Fdistributed-systems#_8-系统高可用要做哪些事",[1482],"先定义目标：RTO（多久恢复）和 RPO（能丢多少数据），不同业务差异很大。然后从冗余、故障转移、隔离和演练四方面设计。 冗余包括多实例、多可用区、多副本；故障转移要有健康检查、选主和流量摘除，并防止脑裂——常用多数派、租约和 fencing 机制。隔离包括线程池、连接池、机房和租户隔离，避免一个依赖拖垮全局。 降级和限流是「保命」手段：非核心功能可关闭，核心链路保留最小依赖。所有降级开关要提前演练，不能等到故障时才发现配置不生效。 最后是演练和演练后的修复：混沌工程、故障注入、备份恢复演练。没有演练过的高可用方案，通常只是文档上的高可用。",{"id":6042,"title":501,"titles":6043,"content":6044,"level":614},"\u002Fjava-interview\u002Fdistributed-systems#易错点与自测",[1482],"CAP 不是「三选二」，分区发生时才是真正的取舍点；BASE 是最终一致，不是「不一致也行」。 雪花 ID 的趋势递增依赖时钟，回拨处理必须明确。 分库分表能解决容量，但会把查询和事务复杂度推到应用层。 自测：① 解释为什么 UUID 不适合做 InnoDB 主键；② 说明一致性哈希中虚拟节点解决什么问题；③ 给出一个「并发重复请求」的幂等落地方案。",{"id":6046,"title":577,"titles":6047,"content":6048,"level":614},"\u002Fjava-interview\u002Fdistributed-systems#参考资料",[1482],"ZooKeeper 官方文档：程序员指南 — znode、watch 与会话语义 Apache ShardingSphere 文档 — 分片、路由与分布式事务 etcd 官方文档 — Raft、租约与 watch 的对照参考 Google SRE Book：处理过载 — 限流、降级与容量设计",{"id":2043,"title":1706,"titles":6050,"content":6051,"level":1206},[],"沿着 put、扩容和树化三条主线拆解 JDK 8 HashMap，讲清下标计算、阈值设计以及并发场景下的真实风险 HashMap 几乎是必问题。只背「数组 + 链表 + 红黑树」只能拿到及格分，能讲清为什么这样设计、并发时具体坏在哪里，才是区分度所在。以下以 JDK 8 的实现为基准，JDK 17\u002F21 的实现主体没有变化。",{"id":6053,"title":1715,"titles":6054,"content":6055,"level":614},"\u002Fjava-interview\u002Fhashmap#_1-hashmap-的底层结构是什么",[1706],"JDK 8 使用「数组 + 链表 + 红黑树」：数组的每个槽位称为桶，哈希冲突的元素先以链表挂载，链表足够长时转为红黑树。 关键常量：默认容量 16、最大容量 1 \u003C\u003C 30、默认负载因子 0.75、树化阈值 8、退化阈值 6、树化的最小容量 64。 节点是 Node\u003CK,V>，保存 hash、key、value 和 next；树化后变成 TreeNode，额外维护红黑树所需的父子指针和颜色，所以单个节点的内存开销明显更大。 JDK 7 只有数组加链表，且插入使用头插法；JDK 8 改为尾插，并引入红黑树，这是两个版本最值得说明的差异。",{"id":6057,"title":1743,"titles":6058,"content":6059,"level":614},"\u002Fjava-interview\u002Fhashmap#_2-哈希扰动和下标计算为什么这样设计",[1706],"取 key 的 hashCode() 后，先做一次扰动：h ^ (h >>> 16)，把高 16 位异或到低 16 位。随后用 (n - 1) & hash 定位桶，其中 n 是数组长度。 因为容量是 2 的幂，(n - 1) 的低位全是 1，与运算等价于取模但更快。问题在于：如果不扰动，只有低位参与定位，高位差异被完全丢弃，大量 key 会挤进少数桶。 扰动让高位也参与运算，以很小的成本改善分布。注意这并不能消除冲突，hashCode 设计糟糕时依旧会退化，所以不要试图用 HashMap 抵抗恶意构造的哈希碰撞。",{"id":6061,"title":1776,"titles":6062,"content":6063,"level":614},"\u002Fjava-interview\u002Fhashmap#_3-put-的完整流程是什么",[1706],"先判断 table 是否为空，为空则初始化；桶为空就直接放新节点；桶首节点的 hash 相同且 key 相等，就覆盖 value。 否则分两种情况：树节点走 putTreeVal；链表则从头部遍历到尾部，途中发现相等 key 就覆盖，没找到就追加到尾部，并检查链表长度是否达到树化门槛。 插入完成后，如果元素总数超过阈值 容量 × 0.75，执行扩容。putIfAbsent、computeIfAbsent、merge 都复用了这套流程，区别只在命中时是否覆盖以及返回值的语义。 面试还可以补一句：key 为 null 时固定放在 0 号桶，这也是 HashMap 与 ConcurrentHashMap 的一个行为差异。",{"id":6065,"title":1817,"titles":6066,"content":6067,"level":614},"\u002Fjava-interview\u002Fhashmap#_4-扩容是怎么做的jdk-7-和-8-有什么差别",[1706],"扩容时容量翻倍，阈值也翻倍；元素数超过 容量 × 0.75 触发，树化失败但容量不足 64 时也会先扩容。 JDK 8 不需要重新计算哈希：把旧桶拆成低位和高位两条链，判断 (e.hash & oldCap)，为 0 留在原下标 j，否则移到 j + oldCap。这样既省掉重算，又保持了链表内部的相对顺序。 JDK 7 会对每个节点重新计算索引，并且用头插法迁移，并发扩容时可能让两个线程把链表连成环，之后 get 死循环。JDK 8 改尾插后环的问题基本消失，但数据覆盖、size 不准、丢更新依然存在。",{"id":6069,"title":1844,"titles":6070,"content":6071,"level":614},"\u002Fjava-interview\u002Fhashmap#_5-为什么容量是-2-的幂负载因子为什么是-075",[1706],"容量为 2 的幂有两个直接好处：(n - 1) & hash 可以替代取模，位运算更快；扩容时只需看哈希的某一位，就能决定元素留在原位还是移动旧容量个位置。 负载因子 0.75 是时间与空间的折中：调低会减少冲突但浪费空间、增加扩容次数；调高则节省空间但链表变长。源码注释给出的依据是，在 0.75 附近、哈希均匀分布时，单个桶内节点数近似服从参数 0.5 的泊松分布。 实际使用时，如果能预估元素数量，直接指定初始容量更有效。注意构造函数会把容量向上取整到 2 的幂，但 HashMap(int initialCapacity) 并不会自动除以 0.75，所以想放 n 个元素，初始容量应给到 n \u002F 0.75 + 1。",{"id":6073,"title":1867,"titles":6074,"content":6075,"level":614},"\u002Fjava-interview\u002Fhashmap#_6-链表树化的阈值为什么是-8退化又是-6",[1706],"树化需要同时满足两个条件：链表长度达到 8，且数组容量不小于 64。后者不满足时只扩容、不树化，因为小表里冲突更多是容量不足造成的。 阈值 8 来自概率：哈希均匀时桶内节点数服从泊松分布，长度达到 8 的概率约为千万分之六，正常情况几乎不会触发。红黑树节点约为普通节点的两倍，只有极端冲突时树化才划算。 退化阈值设为 6 而不是 8，是为了留出缓冲带，避免在 7、8 之间反复树化与退化。节点删除后，当树的结构不足以维持时会退化为链表；扩容拆分时，如果某条链上的节点数不超过 6，也会退回链表。",{"id":6077,"title":1880,"titles":6078,"content":6079,"level":614},"\u002Fjava-interview\u002Fhashmap#_7-hashmap-为什么线程不安全",[1706],"并发 put 可能同时判断某个桶为空，然后各自写入，后写覆盖先写；两个线程同时扩容时，可能基于旧表复制，导致部分节点丢失或 size 计数错误。 JDK 7 的头插式扩容还可能形成环形链表，CPU 打满；JDK 8 改为尾插并保留相对顺序，环的问题基本不复现，但「不安全」这个结论并没有改变。 Hashtable 是方法级 synchronized，粒度太粗；Collections.synchronizedMap 也是整表锁。并发场景优先 ConcurrentHashMap，它的读基本无锁，写只锁桶，并且通过 sizeCtl 协调扩容。 还要注意：即使换成 ConcurrentHashMap，if (!map.containsKey(k)) map.put(k, v) 仍不是原子的，应改用 putIfAbsent 或 computeIfAbsent。",{"id":6081,"title":2037,"titles":6082,"content":6083,"level":614},"\u002Fjava-interview\u002Fhashmap#_8-作为-key-的对象要满足什么条件如何用-linkedhashmap-实现-lru",[1706],"key 必须正确实现 equals 和 hashCode，并且哈希值在放入后保持稳定。如果用可变对象做 key，修改了参与哈希计算的字段，之后按原值就查不到了，等于在表里制造了一个「幽灵条目」。 LinkedHashMap 在哈希表之上维护一条双向链表。构造时传 accessOrder = true，每次 get 或 put 命中都会把节点移到链表尾部，头部自然成为最久未使用的元素。 再重写 removeEldestEntry，在元素数超过容量时返回 true，就得到一个简单的 LRU。生产环境要注意：访问顺序模式下 get 也会修改结构，遍历时需要额外同步；容量估算、过期时间和并发控制通常还需要外面再包一层。",{"id":6085,"title":501,"titles":6086,"content":6087,"level":614},"\u002Fjava-interview\u002Fhashmap#易错点与自测",[1706],"「链表长度到 8 就树化」不准确，还要容量至少 64；「缩到 6 就退化」也取决于删除路径与树结构。 初始容量不等于能装多少元素，阈值是容量乘以负载因子。 HashMap 线程不安全的表现不止死循环，数据覆盖和计数错误更常见。 自测：① 手写 (n - 1) & hash 并解释为什么等价于取模；② 说明扩容时高低位拆分的判断条件；③ 给出一个用 LinkedHashMap 实现 LRU 的最小骨架。",{"id":6089,"title":577,"titles":6090,"content":6091,"level":614},"\u002Fjava-interview\u002Fhashmap#参考资料",[1706],"HashMap 源码（OpenJDK） — 常量、putVal、resize 与 treeifyBin java.util.HashMap API（Java 21） — 允许 null 键值与非线程安全的官方说明 java.util.concurrent.ConcurrentHashMap API — 并发语义与复合操作",{"id":2491,"title":2050,"titles":6093,"content":6094,"level":1206},[],"从 == 与 equals、字符串不可变、包装类缓存、值传递到泛型擦除，梳理 Java 基础题的高频追问和版本边界 基础题最容易答成背诵。下面每题都按「结论 → 原理 → 追问」组织，重点是把 JLS 里的规则和日常踩过的坑对应起来。",{"id":6096,"title":2476,"titles":6097,"content":6098,"level":614},"\u002Fjava-interview\u002Fjava-basics#_1-和-equals-有什么区别",[2050],"== 比较值本身：基本类型比较数值，引用类型比较是否指向同一个对象。equals 是 Object 的方法，默认实现就是 ==，但 String、包装类、集合等重写了它，改成比较内容。 重写 equals 必须同时重写 hashCode，否则两个相等的对象可能算进不同的桶。契约是：equals 为真则 hashCode 必须相等；反过来不要求。违反契约会让 HashMap、HashSet 出现「放进去却查不到」的诡异现象。 追问常见于浮点数：0.1 + 0.2 == 0.3 为 false，因为二进制无法精确表示十进制小数。金额应使用 BigDecimal，并且用字符串构造，否则从 double 转入时误差已经带入。",{"id":6100,"title":2478,"titles":6101,"content":6102,"level":614},"\u002Fjava-interview\u002Fjava-basics#_2-为什么-string-设计成不可变三种字符串怎么选",[2050],"String 类是 final 的，内部字符存储在私有字段中（JDK 9 起改为 byte[] 加编码标记，以压缩拉丁字符占用），所有看似修改的方法都返回新对象。 不可变换来四件事：可以安全共享、可以缓存哈希值（所以适合当 HashMap 的 key）、天然线程安全、传参后不会被调用方篡改。代价是频繁拼接会产生大量临时对象。 选型上：常量或少量拼接用 String；循环内拼接用 StringBuilder；确实需要多线程共享同一个可变缓冲区时才用 StringBuffer。绝大多数场景里，把 StringBuilder 限制在方法内部比用同步版本更有效。",{"id":6104,"title":2480,"titles":6105,"content":6106,"level":614},"\u002Fjava-interview\u002Fjava-basics#_3-new-stringabc-会创建几个对象",[2050],"分情况：若常量池中还没有 \"abc\"，类加载解析字面量时先创建池中对象，执行 new 时再在堆上创建实例，共两个；若池中已有，则只创建堆上那一个。面试时把两种口径讲清楚，比背「两个」更稳妥。 intern() 的语义是：池中有相等字符串就返回池中引用，否则把当前字符串的引用登记进去并返回。JDK 7 起常量池移到堆中，所以 new String(\"a\").intern() == \"a\" 为 true。 编译期能折叠的只有字面量拼接，例如 \"a\" + \"b\" 会直接变成 \"ab\"。而 new String(\"a\") + new String(\"b\") 是运行期拼接，结果不会自动进入常量池。",{"id":6108,"title":2212,"titles":6109,"content":6110,"level":614},"\u002Fjava-interview\u002Fjava-basics#_4-包装类的缓存与自动拆箱有哪些坑",[2050],"自动装箱调用 Integer.valueOf，默认缓存 -128 到 127，可用 -XX:AutoBoxCacheMax 调大上界。因此两个值为 127 的 Integer 用 == 比较为 true，128 则为 false；比较值一律用 equals。 最常见的坑是空指针：Integer x = null; x == 1 会拆箱，直接抛 NPE。Map.get() 返回 null 后赋给 int、三目表达式两边类型不一致导致整体提升，也都会引发同类问题。 Long 的缓存范围固定为 -128 到 127，不受上述参数影响。金额、订单号这类业务标识不要依赖缓存行为，统一用值比较或明确的基本类型。",{"id":6112,"title":2266,"titles":6113,"content":6114,"level":614},"\u002Fjava-interview\u002Fjava-basics#_5-java-是值传递还是引用传递",[2050],"Java 只有值传递。基本类型传的是数值副本；引用类型传的是引用的副本，也就是对象地址的拷贝。 所以方法内可以通过这个副本修改对象内容，但把形参重新指向新对象，不会影响调用方的引用。典型例子是：给 StringBuilder 追加内容能生效，而在方法里 sb = new StringBuilder() 之后的操作与外部无关。 追问常考「为什么交换两个对象不生效」：交换的只是两个引用副本。交换数组元素则能生效，因为修改的是数组对象内部的数据。",{"id":6116,"title":2286,"titles":6117,"content":6118,"level":614},"\u002Fjava-interview\u002Fjava-basics#_6-泛型擦除带来了哪些限制pecs-怎么理解",[2050],"Java 泛型是编译期特性，编译后类型参数被擦除到上界（无界时为 Object），运行时拿不到 List\u003CString> 的具体类型实参。 由此产生几条限制：不能 new T()、不能直接创建泛型数组、不能写 instanceof List\u003CString>、静态成员不能引用类上的类型参数、基本类型不能作为类型实参。为了维持多态，编译器会生成桥方法。 通配符遵循 PECS：生产者用 ? extends T，只能读不能写；消费者用 ? super T，可以写但读出来只能是 Object。List\u003C?> 不能 add 任何非 null 元素。 还要区分：List\u003CInteger> 不能赋给 List\u003CNumber>，这不是擦除造成的，而是泛型不协变。数组是协变的，所以 Object[] arr = new String[1] 能通过编译，但写入其他类型会在运行期抛 ArrayStoreException。",{"id":6120,"title":2351,"titles":6121,"content":6122,"level":614},"\u002Fjava-interview\u002Fjava-basics#_7-重载和重写有什么区别多态靠什么实现",[2050],"重载在编译期按静态类型和参数列表选定，返回值不同不构成重载；重写在运行期按对象实际类型分派。重写要求签名相同、返回类型协变、访问权限不能收窄、不能抛出更宽的受检异常。 多态依赖对象头里的类型元数据指针和方法表：调用虚方法时按实际类型找到实现。private、static、final 方法不参与动态分派。 两个容易忽略的点：一是在构造器里调用可重写方法，子类字段尚未初始化，可能读到默认值；二是重载在编译期就固定，父类静态类型看不到子类新增的重载版本。",{"id":6124,"title":2373,"titles":6125,"content":6126,"level":614},"\u002Fjava-interview\u002Fjava-basics#_8-从-java-8-到-21哪些语言特性值得在面试里讲",[2050],"JDK 8 带来 Lambda、Stream、Optional、接口默认方法和新的日期时间 API。JDK 10 的 var 只做局部变量类型推断，不改变类型系统。JDK 11 标准化了 HttpClient。JDK 17 是主流 LTS，包含 record、密封类、instanceof 模式匹配、文本块和 switch 表达式。 JDK 21 值得关注 switch 模式匹配（含 record 解构）、虚拟线程、分代 ZGC 和有序集合接口。 回答时最好落到场景：record 适合 DTO 和值对象，不适合需要可变状态或依赖无参构造的框架实体；虚拟线程适合大量阻塞在 IO 上的任务，不适合 CPU 密集任务，也不适合长时间持有 synchronized 的临界区——JDK 21 下会钉住载体线程，JDK 24 才改善这一限制。",{"id":6128,"title":501,"titles":6129,"content":6130,"level":614},"\u002Fjava-interview\u002Fjava-basics#易错点与自测",[2050],"不要只背「String 不可变所以线程安全」，要能说出共享、哈希缓存、常量池三个具体收益。 包装类比较、null 拆箱、BigDecimal 构造方式，是笔试里最高频的失分点。 泛型擦除不等于「运行时完全没有泛型信息」，声明处的信息仍可通过反射读到。 自测：① 解释 Integer a = 128; Integer b = 128; a == b 的结果；② 说明为什么泛型不能协变而数组可以；③ 说出 JDK 21 虚拟线程不适合哪两类任务。",{"id":6132,"title":577,"titles":6133,"content":6134,"level":614},"\u002Fjava-interview\u002Fjava-basics#参考资料",[2050],"JLS SE 21：类型、转换与泛型 — 泛型擦除、装箱转换的规范定义 java.lang.Object API — equals 与 hashCode 的契约原文 JEP 444：Virtual Threads — 虚拟线程的适用场景与钉住问题 JEP 441：Pattern Matching for switch — switch 模式匹配的正式语义",{"id":2758,"title":2498,"titles":6136,"content":6137,"level":1206},[],"从运行时数据区、对象布局到三色标记和 G1\u002FZGC 选型，串起 JVM 面试题与 OOM、GC 日志的排查方法 JVM 题的典型答法是背名词。更好的方式是把「对象放在哪、什么时候回收、回收时怎么保证正确、出问题怎么取证」这条链路讲通。以下以 HotSpot、JDK 17\u002F21 为语境。",{"id":6139,"title":2507,"titles":6140,"content":6141,"level":614},"\u002Fjava-interview\u002Fjvm#_1-jvm-运行时数据区怎么划分",[2498],"按线程私有和共享划分。私有部分包括程序计数器、虚拟机栈和本地方法栈：程序计数器记录当前字节码位置，是唯一不会 OOM 的区域；虚拟机栈存放栈帧，包含局部变量表、操作数栈和返回地址，递归过深会抛 StackOverflowError。 共享部分是堆和方法区。堆存放对象实例，是 GC 的主战场；方法区在 HotSpot 中由元空间实现，存放类元数据、运行时常量池等，JDK 8 起从永久代迁到本地内存。 此外还有直接内存，它不属于运行时数据区，但 NIO 的 DirectByteBuffer 会占用，受 -XX:MaxDirectMemorySize 影响，超限时报 Direct buffer memory。字符串常量池在 JDK 7 已移到堆中。",{"id":6143,"title":2535,"titles":6144,"content":6145,"level":614},"\u002Fjava-interview\u002Fjvm#_2-一个对象从创建到回收经历了什么",[2498],"new 之后先检查类是否已加载，然后在堆上分配内存。分配通常用指针碰撞或空闲列表，多线程下通过 TLAB 减少竞争；分配后先把实例字段清零，再设置对象头，最后执行构造器。 对象布局包括 Mark Word、类型指针、实例数据和对齐填充。Mark Word 存哈希、GC 年龄和锁状态；开启指针压缩后引用占 4 字节，堆小于约 32GB 时默认开启。 新对象一般进入 Eden。Eden 满时触发 Young GC，存活对象复制到 Survivor 并增加年龄，达到阈值后晋升老年代；大对象可能直接进老年代。老年代压力过大时触发 Full GC 或混合回收。",{"id":6147,"title":2550,"titles":6148,"content":6149,"level":614},"\u002Fjava-interview\u002Fjvm#_3-如何判断对象可以回收三色标记解决了什么问题",[2498],"主流 JVM 用可达性分析：从 GC Roots 出发遍历引用链，不可达的对象才可回收。GC Roots 包括栈上的局部变量、静态字段、JNI 引用、活动线程和同步监视器等。 引用强度分强、软、弱、虚：软引用在内存不足时回收，弱引用下次 GC 就回收，虚引用只用于回收通知。finalize 已废弃，不应作为资源释放手段。 并发标记时用户线程仍在改引用，三色标记可能漏标：黑对象新增了指向白对象的引用，或灰对象到白对象的引用被删除。CMS 用增量更新记录新增引用，G1 用 SATB 记录删除前的快照，两者都靠写屏障实现。",{"id":6151,"title":2567,"titles":6152,"content":6153,"level":614},"\u002Fjava-interview\u002Fjvm#_4-g1-和-cmszgc-有什么区别",[2498],"CMS 以低停顿为目标，但存在碎片和并发模式失败，已在 JDK 14 移除。G1 从 JDK 9 起是默认收集器，把堆划分成大小相等的 Region，年轻代和老年代是逻辑集合而非物理连续区域，可以按回收价值优先选择 Region。 G1 有 Young GC、Mixed GC 和必要的 Full GC。-XX:MaxGCPauseMillis 是软目标，不是硬保证；大对象（超过 Region 一半）会进入 Humongous 区域。跨 Region 引用靠记忆集和卡表维护。 ZGC 用染色指针和读屏障实现并发标记与并发整理，停顿与堆大小基本无关，JDK 21 引入分代 ZGC。选型不应只看「谁更新」：吞吐敏感先看 Parallel，延迟敏感看 G1 或 ZGC，还要结合堆大小、CPU 和 JDK 版本压测。",{"id":6155,"title":2584,"titles":6156,"content":6157,"level":614},"\u002Fjava-interview\u002Fjvm#_5-类加载过程和双亲委派模型",[2498],"类的生命周期是加载、链接（验证、准备、解析）、初始化、使用、卸载。准备阶段给静态变量分配内存并设零值，static final 常量在编译期已写入常量池；初始化阶段才执行 \u003Cclinit>，包括静态赋值和静态块。 双亲委派指加载请求先交给父加载器，父加载器无法完成才自己加载。它保证核心类库不被同名类替换，也避免重复加载。启动类加载器、平台类加载器、应用类加载器是三层常见结构。 委派可以被打破：SPI 通过线程上下文类加载器加载实现类；Tomcat 为每个 Web 应用维护独立加载器，实现应用隔离；OSGi 则是网状委派。注意「被动引用」不会触发初始化，例如通过子类访问父类静态字段、访问编译期常量。",{"id":6159,"title":2605,"titles":6160,"content":6161,"level":614},"\u002Fjava-interview\u002Fjvm#_6-有哪些-oom线上怎么排查",[2498],"常见类型：堆空间不足、GC 开销超限、元空间不足（动态生成类过多）、直接内存不足、无法创建本地线程（线程数或栈过大）、数组长度超限。 排查顺序：先确认是哪种 OOM，再看是否有 -XX:+HeapDumpOnOutOfMemoryError 留下的堆转储；用 jcmd \u003Cpid> GC.heap_info 看堆使用，用 jmap -histo:live 看对象分布，必要时导出 hprof 用 MAT 找支配树和 GC Root 路径。 还要区分「内存泄漏」和「内存不够」：前者是对象该回收却仍被引用，需要定位持有链；后者可能是缓存无上限、批量查询过大或堆设置不合理。直接内存和线程数问题不会体现在堆转储里，要看 NMT 和系统级指标。",{"id":6163,"title":2630,"titles":6164,"content":6165,"level":614},"\u002Fjava-interview\u002Fjvm#_7-jit-编译和逃逸分析对性能有什么影响",[2498],"HotSpot 采用解释器加分层编译：热点方法先由 C1 快速编译，执行更久后交给 C2 做激进优化。方法内联、循环展开、常量折叠、去虚拟化都会显著影响峰值性能，所以短跑基准测试往往失真。 逃逸分析判断对象是否逃出方法或线程。不逃逸的对象可以标量替换，把字段拆成局部变量，从而避免堆分配；锁对象不逃逸时可以做锁消除。它不等于「对象一定分配在栈上」，HotSpot 主要做的是标量替换。 实际影响：压测前要预热，避免把解释执行阶段算进吞吐；不要用过于复杂的继承层次阻碍内联；-Xint 只用于排查，不适合生产。",{"id":6167,"title":2647,"titles":6168,"content":6169,"level":614},"\u002Fjava-interview\u002Fjvm#_8-jvm-参数怎么调gc-日志怎么看",[2498],"先定堆：-Xms 和 -Xmx 设成相同值，避免运行期反复扩缩；容器环境用 -XX:MaxRAMPercentage 按比例设置，并给堆外内存和元空间留余量。再设 -XX:MaxMetaspaceSize、开启 OOM 堆转储。 GC 参数从业务目标出发：延迟优先可设 -XX:MaxGCPauseMillis，但不要设得过小导致 Young 区被压缩、回收更频繁。别照抄网上的「万能参数」，不同 JDK 的默认收集器和日志参数都不一样。 JDK 9 起用统一日志：-Xlog:gc*:file=gc.log:time,uptime,level,tags。重点看停顿时间、回收前后占用、晋升量和 Full GC 频率。单次 Young GC 变长可能是存活对象变多，频繁 Full GC 往往要看是不是晋升过快或存在泄漏。",{"id":6171,"title":501,"titles":6172,"content":6173,"level":614},"\u002Fjava-interview\u002Fjvm#易错点与自测",[2498],"永久代已在 JDK 8 移除，元空间在本地内存，不等同于「没有上限」。 G1 的 Region 不等于物理连续的新生代\u002F老年代，不要把分代模型硬套到内存布局上。 堆 dump 只能解释堆内问题，直接内存、线程栈、元空间要另找证据。 自测：① 说明 CMS 与 G1 处理漏标的方式差异；② 给出一组容器内 4C8G 服务的 JVM 参数并解释理由；③ 描述一次「堆内存持续上涨」的完整取证步骤。",{"id":6175,"title":577,"titles":6176,"content":6177,"level":614},"\u002Fjava-interview\u002Fjvm#参考资料",[2498],"Java SE 21 GC 调优指南 — 收集器选择、G1 与 ZGC 的停顿目标 JVM 规范 SE 21 — 运行时数据区与类加载的规范定义 jcmd 手册（Java 21） — Thread.print、GC.heap_info、GC.heap_dump、JFR.start 的语法与影响 JEP 439：Generational ZGC — 分代 ZGC 的设计目标",{"id":2992,"title":2765,"titles":6179,"content":6180,"level":1206},[],"对比 RabbitMQ、RocketMQ、Kafka 的能力边界，讲清不丢消息、重复消费、顺序、积压和事务消息的工程解法 消息队列的题很少考 API，更多考「你敢不敢把它放进核心链路」。回答时先讲它带来的三个收益，再讲它引入的四个新问题：一致性、重复、顺序和运维复杂度。",{"id":6182,"title":2774,"titles":6183,"content":6184,"level":614},"\u002Fjava-interview\u002Fmessage-queues#_1-为什么要用消息队列引入了哪些问题",[2765],"三个典型收益：解耦（生产者不需要知道有多少下游）、削峰（把突发流量缓冲到消费端能承受的速率）、异步（把非关键步骤移出主链路，缩短响应时间）。 代价同样明显：系统从「一次调用」变成「最终一致」，需要处理消息丢失、重复、乱序和积压；链路变长后，排查问题需要消息 id 贯穿；中间件本身也需要高可用、监控和容量规划。 所以不是所有调用都适合异步。强一致、需要立即返回结果、调用方必须知道成败的场景，同步调用更简单。用 MQ 之前先问：业务能否接受延迟和最终一致？ 一个常见反模式是「为了解耦而拆出十几个消费者」，结果每次改消息格式都要协调所有下游，耦合只是从代码转移到了契约。",{"id":6186,"title":2790,"titles":6187,"content":6188,"level":614},"\u002Fjava-interview\u002Fmessage-queues#_2-rabbitmqrocketmqkafka-怎么选",[2765],"RabbitMQ 基于 Erlang 和 AMQP，路由能力最强，支持 direct、topic、fanout、headers 等交换机模型，单条消息确认和灵活的重试、死信机制成熟，适合业务消息和中小规模异步。 RocketMQ 是 Java 生态，吞吐高，原生支持顺序消息、事务消息、延迟消息和死信队列，比较贴合电商、订单这类业务场景；运维和生态相比 Kafka 稍窄。 Kafka 以分区日志为核心，吞吐最高，适合日志、埋点、流处理和数据管道；它按分区保证顺序，消费位点由客户端管理，缺少原生的单条重试和死信，业务语义要自己补。 选型要看「消息形态」：要复杂路由和逐条确认，选 RabbitMQ；要事务消息和业务顺序，选 RocketMQ；要大吞吐的流式数据，选 Kafka。三者都能做到百万级，不要只拿基准测试的峰值做决定。",{"id":6190,"title":2806,"titles":6191,"content":6192,"level":614},"\u002Fjava-interview\u002Fmessage-queues#_3-如何保证消息不丢失",[2765],"要分三段看：生产、存储、消费。 生产端：RabbitMQ 用 publisher confirm 加持久化交换机\u002F队列；RocketMQ 用同步发送并检查 SendStatus，或事务消息；Kafka 设置 acks=all，配合 min.insync.replicas 和重试。异步发送必须处理回调失败，不能发出去就不管。 存储端：消息和队列都要持久化，副本数至少 2 到 3，并确认刷盘策略。同步刷盘更安全但更慢，异步刷盘在宕机时可能丢最近一批。 消费端：处理成功后再确认。RabbitMQ 用 basicAck，RocketMQ 返回 CONSUME_SUCCESS，Kafka 在业务完成后提交 offset。先确认后处理，进程崩溃就会丢消息。 即便如此，也只能做到「尽量不丢」，所以关键业务还需要对账和补偿。",{"id":6194,"title":2845,"titles":6195,"content":6196,"level":614},"\u002Fjava-interview\u002Fmessage-queues#_4-如何保证消息不被重复消费",[2765],"先明确：端到端严格一次很难做到。生产端重试、网络超时、消费者在「处理成功但确认失败」时重投，都会产生重复，工程上普遍采用「至少一次投递 + 消费端幂等」。 幂等的常见实现：业务唯一键加数据库唯一索引；去重表记录消息 id；Redis SET NX 做短期去重；状态机只允许特定状态迁移；乐观锁版本号。选择哪种取决于业务，金额类操作更适合唯一约束和流水表。 Kafka 的事务和幂等生产者能保证「Kafka 内部」的写入不重复，但跨数据库的副作用仍要自己做幂等。RocketMQ 的事务消息解决的是「本地事务与发消息」的原子性，不是消费去重。 面试时最好补一句：去重表要设置合理的保留期，并考虑并发下先插入还是先处理。",{"id":6198,"title":2864,"titles":6199,"content":6200,"level":614},"\u002Fjava-interview\u002Fmessage-queues#_5-顺序消息怎么实现",[2765],"顺序的前提是「同一业务键的消息进入同一队列或分区，并且只被一个消费者按序处理」。 Kafka 用消息 key 做分区，同一 key 落到同一 partition，消费者组内一个分区只分配给一个消费者；RocketMQ 用 MessageQueueSelector 选择队列，消费端用顺序监听器。 破坏顺序的常见原因：生产端重试后消息落到其他分区；消费端多线程并行处理；失败重试把消息挪到队尾；扩容导致分区重新分配。所以顺序消息通常要配合「失败阻塞重试」或把失败消息转入专门的重试队列，避免影响后续消息。 还要评估是否真的需要全局顺序。绝大多数场景只需要「同一订单\u002F同一用户」的局部顺序，全局单分区会牺牲吞吐。",{"id":6202,"title":2884,"titles":6203,"content":6204,"level":614},"\u002Fjava-interview\u002Fmessage-queues#_6-消息积压了怎么办",[2765],"先判断原因：消费者挂了、消费变慢（下游数据库或接口）、生产突增、分区数不足，还是消息体过大。 短期止损可以扩容消费者，但 Kafka 的并行度受分区数限制，RocketMQ 受队列数限制，扩容前先确认。必要时临时把积压消息转投到一个分区更多的新 topic，用更多消费者快速消化，再切回正常链路。 消费端可以做批量拉取、批量写库、减少同步远程调用；非核心消息可以降级或延后处理。同时要防止「为了追平而无限并发」把数据库打垮。 长期要做容量评估和监控：Kafka 看 consumer lag，RocketMQ 看堆积量，RabbitMQ 看队列深度。积压告警要带速率和预计消化时间，而不只是一个绝对数字。",{"id":6206,"title":2900,"titles":6207,"content":6208,"level":614},"\u002Fjava-interview\u002Fmessage-queues#_7-事务消息和本地消息表怎么选",[2765],"RocketMQ 事务消息的流程是：先发半消息，执行本地事务，再根据结果提交或回滚；如果 Broker 长时间收不到确认，会回查生产者的本地事务状态。 它解决的是「本地事务成功但消息没发出去」或「消息发出但本地事务回滚」的问题，适合已有 RocketMQ 且业务链路相对简单的场景。 本地消息表把待发消息和业务数据放在同一个数据库事务里，再由定时任务或 CDC 投递。它不依赖 MQ 的事务能力，跨 MQ 也能用，代价是要多一张表、需要重试和清理，并且投递是异步的。 两者都只能保证最终一致。选择时看团队对中间件的掌握程度、是否已有可靠的调度和对账体系。不要让「事务消息」变成「分布式强一致」的错觉。",{"id":6210,"title":2916,"titles":6211,"content":6212,"level":614},"\u002Fjava-interview\u002Fmessage-queues#_8-死信延迟消息和重试应该怎么设计",[2765],"重试要分级：瞬时故障（网络抖动、下游限流）适合指数退避加抖动；业务性失败（参数错误、库存不足）不应无限重试，直接进死信并告警。 延迟消息适合「超时未支付关单」「定时提醒」这类场景。RabbitMQ 可以用 TTL 加死信交换机，或延迟插件；RocketMQ 支持延迟级别和 5.0 的定时消息；Kafka 没有原生延迟，需要外部调度或时间轮。 死信队列不是垃圾桶：要能查询、告警、人工重放，并保留原始消息和失败原因。重放接口要做权限和幂等保护，避免把故障放大。 还要防「毒消息」：某条消息必然处理失败时，如果一直重试会阻塞整个队列。顺序队列尤其要设置最大重试次数，超过后转死信，保证后续消息能继续。",{"id":6214,"title":501,"titles":6215,"content":6216,"level":614},"\u002Fjava-interview\u002Fmessage-queues#易错点与自测",[2765],"「消息不丢」是三段式问题，只答 Broker 持久化不完整。 「不重复消费」本质是消费端幂等，别承诺端到端 exactly-once。 顺序、积压、重试三者互相影响，扩容分区可能破坏顺序。 自测：① 画出 RocketMQ 事务消息的半消息与回查流程；② 说明 Kafka 中 acks=all 仍可能丢消息的条件；③ 给出一个「消费成功但确认失败」的幂等方案。",{"id":6218,"title":577,"titles":6219,"content":6220,"level":614},"\u002Fjava-interview\u002Fmessage-queues#参考资料",[2765],"Apache Kafka 官方文档 — 分区、副本、acks 与事务语义 Apache RocketMQ 官方文档 — 顺序消息、事务消息、延迟消息与重试 RabbitMQ 官方文档：可靠性指南 — 确认、持久化与死信",{"id":3220,"title":2999,"titles":6222,"content":6223,"level":1206},[],"从服务边界、Spring Cloud 组件到 Dubbo、超时重试与分布式事务，梳理微服务面试的主线与常见误区 微服务题的重点不是组件清单，而是「拆不拆、怎么通、出错怎么办」。下面按这三个问题展开，组件只作为实现手段出现。",{"id":6225,"title":3008,"titles":6226,"content":6227,"level":614},"\u002Fjava-interview\u002Fmicroservices#_1-微服务应该怎么拆什么时候不该拆",[2999],"常见依据是业务能力与限界上下文：一个服务应拥有相对完整的一块业务和它的数据，能独立部署、独立演进。也可以按组织结构和变更频率拆，让经常一起改的代码待在一起。 判断边界时问三个问题：这块能力是否有独立的生命周期？是否有清晰的数据所有权？跨边界的调用是否足够少？如果一个变更总要同时改五个服务，说明边界切错了。 不该拆的情况同样重要：团队规模小、领域还没稳定、强事务要求高、流量很低。此时模块化单体往往更划算，等边界清晰后再拆，比一开始拆错再合并容易。 还要警惕「按技术分层拆」：用户服务、订单服务之外再拆出「查询服务」「写入服务」，会把一次业务变更拉成跨团队协作，收益通常抵不过成本。",{"id":6229,"title":3024,"titles":6230,"content":6231,"level":614},"\u002Fjava-interview\u002Fmicroservices#_2-spring-cloud-常见组件分别解决什么问题",[2999],"网关负责统一入口、路由、鉴权、限流和灰度；注册中心负责服务实例的注册与发现，常见有 Nacos、Consul、Eureka；客户端负载均衡负责在实例列表中选择目标，Spring Cloud LoadBalancer 已替代 Ribbon。 声明式调用用 OpenFeign，把 HTTP 调用封装成接口；容错限流可以用 Sentinel 或 Resilience4j；配置中心负责动态配置；链路追踪在 Spring Boot 3 时代通常用 Micrometer Tracing 对接 OpenTelemetry。 要注意版本配套：Spring Boot 3 \u002F Spring Cloud 2022+ 使用 Jakarta 命名空间，旧版 Sleuth 已不再是主线。选组件时优先看团队是否真的会用，而不是把全家桶一次性引入。 组件越多，故障面越大。一个只有三个服务的小系统，可能只需要网关、注册中心和基础监控。",{"id":6233,"title":3040,"titles":6234,"content":6235,"level":614},"\u002Fjava-interview\u002Fmicroservices#_3-服务注册发现和负载均衡是怎么工作的",[2999],"服务启动时把地址、端口、元数据注册到注册中心，并定期发送心跳；消费者拉取或订阅实例列表，缓存在本地，调用时按负载均衡策略选一个实例。 注册中心通常区分临时实例和持久实例：临时实例靠心跳续约，失联后被剔除；持久实例需要显式注销。健康检查有客户端心跳和服务端探测两种方式。 选型会涉及 CAP：Eureka 偏向可用性，允许短时间读到旧列表；ZooKeeper 偏向一致性，网络分区时可能拒绝写；Nacos 默认偏 AP，也提供 CP 模式。 客户端负载均衡不需要额外的代理进程，但每个消费者都要维护实例列表和健康判断。服务端负载均衡（如 Nginx、云负载均衡）把选择集中在一处，运维简单但多一跳。",{"id":6237,"title":3056,"titles":6238,"content":6239,"level":614},"\u002Fjava-interview\u002Fmicroservices#_4-dubbo-和-openfeign-有什么区别spi-有什么不同",[2999],"OpenFeign 基于 HTTP，语义通用、调试方便、跨语言友好，适合对外接口和异构系统；代价是文本协议和每次调用的连接、序列化开销相对高。 Dubbo 是 RPC 框架，默认走二进制协议和 TCP 长连接，性能通常更好，提供服务治理能力（超时、重试、路由、负载均衡、熔断）。它更适合内部高频调用，但要求双方使用兼容的协议和序列化方式。 Dubbo 3 支持 Triple 协议，兼容 gRPC 和 HTTP\u002F2，跨语言能力比早期版本好很多。选型不能只看性能，还要看团队对协议、序列化和治理的掌握程度。 SPI 方面，JDK SPI 通过 META-INF\u002Fservices 加载，一次性实例化所有实现，无法按需选择；Dubbo SPI 用 @SPI 指定默认实现，@Adaptive 做自适应扩展，@Activate 做条件激活，支持按名称获取和包装扩展，灵活得多。",{"id":6241,"title":3088,"titles":6242,"content":6243,"level":614},"\u002Fjava-interview\u002Fmicroservices#_5-超时重试熔断限流应该怎么配",[2999],"超时应该自下而上设置：下游超时小于上游，并留出处理余量。更好的做法是传递截止时间（deadline），让整条调用链共享同一个预算，而不是每层各自设一个固定值。 重试只对幂等操作开启，并且要限制次数、加退避和抖动。同步调用链上层层重试会造成放大效应，三层各重试两次就是八倍流量，容易把已经过载的下游彻底压垮。 熔断器有闭合、打开、半开三个状态：失败率超阈值后打开，快速失败；经过冷却期进入半开，放少量请求试探。它保护的是调用方，也给了下游恢复时间。 限流常用令牌桶（允许突发）和漏桶（平滑输出），还要考虑单机限流与集群限流的差别。降级方案要提前设计好返回值，不能等故障时临时决定。",{"id":6245,"title":3104,"titles":6246,"content":6247,"level":614},"\u002Fjava-interview\u002Fmicroservices#_6-分布式事务有哪些方案seata-at-的边界在哪",[2999],"强一致方案主要是 2PC\u002FXA：协调者统一提交或回滚，实现简单但有同步阻塞、协调者单点和性能问题。3PC 缓解阻塞，仍不能很好应对网络分区。 柔性事务包括 TCC、Saga、本地消息表和可靠消息。TCC 需要业务实现 Try\u002FConfirm\u002FCancel，要处理空回滚、悬挂和幂等；Saga 用一系列本地事务加补偿，隔离性弱，适合长流程；本地消息表靠最终一致，工程上最常用。 Seata AT 通过解析 SQL 生成前后镜像，提交本地事务并记录 undo log，失败时反向补偿。它默认不提供强隔离，读到中间状态需要配合全局锁或业务设计；复杂 SQL、存储过程、非事务表都可能失效。 选择方案的顺序应该是：能不能不分布式事务（合并服务、调整边界）→ 能否最终一致 → 最后才考虑强一致方案。",{"id":6249,"title":3120,"titles":6250,"content":6251,"level":614},"\u002Fjava-interview\u002Fmicroservices#_7-微服务可观测性要建设哪些能力",[2999],"三根支柱是日志、指标和链路追踪。日志要结构化，并带上 traceId、用户标识和业务单号；指标关注流量、错误率、延迟和饱和度；链路追踪要能串起跨服务调用，定位是哪一跳变慢。 指标命名可以套用 RED（请求速率、错误、耗时）和 USE（使用率、饱和度、错误）；告警要基于症状（用户可感知的错误率和延迟），而不是只看 CPU。 OpenTelemetry 正在成为事实标准，trace context 通过 W3C traceparent 头传递。采样策略要平衡成本与可观测性，错误请求和慢请求应提高采样率。 可观测性不是装个 APM 就结束：要有统一的检索入口、清晰的仪表盘和可执行的 Runbook，否则故障时仍然靠猜。",{"id":6253,"title":3140,"titles":6254,"content":6255,"level":614},"\u002Fjava-interview\u002Fmicroservices#_8-网关层要做哪些事鉴权怎么设计",[2999],"网关适合做与业务无关的横切能力：路由转发、TLS 终止、统一鉴权、限流熔断、跨域、请求日志、灰度分流和协议转换。它不适合承载业务逻辑，否则会成为新的单体。 鉴权可以在网关做粗粒度的身份校验（验签、解析 token、拦截未登录请求），细粒度的权限判断仍应放在服务内，因为服务需要知道「这个用户能否操作这个资源」。 JWT 自带声明、便于水平扩展，但撤销困难；常见做法是短有效期加刷新令牌，敏感操作再查一次权限或黑名单。传统 Session 依赖集中存储，撤销容易但需要共享会话。 内部服务之间建议使用 mTLS 或服务身份令牌，不要默认「内网可信」。网关还要防止把外部请求头原样透传，避免伪造身份字段。",{"id":6257,"title":501,"titles":6258,"content":6259,"level":614},"\u002Fjava-interview\u002Fmicroservices#易错点与自测",[2999],"拆分不是越细越好，数据所有权和变更边界比「服务数量」更重要。 重试是放大器，没有幂等和退避的重试是故障加速器。 分布式事务没有银弹，先问业务能否接受最终一致。 自测：① 说明客户端负载均衡与服务端负载均衡的差异；② 画出熔断器的三个状态及迁移条件；③ 解释 Seata AT 为什么默认不是强隔离。",{"id":6261,"title":577,"titles":6262,"content":6263,"level":614},"\u002Fjava-interview\u002Fmicroservices#参考资料",[2999],"Spring Cloud 官方文档 — 各组件与版本对应关系 Apache Dubbo 官方文档 — 协议、SPI 与服务治理 Seata 官方文档 — AT、TCC、Saga 的模式与约束 OpenTelemetry 文档 — 链路、指标与日志的统一采集",{"id":3664,"title":3227,"titles":6265,"content":6266,"level":1206},[],"讲清 #{} 与 ${} 的区别、Mapper 动态代理、一二级缓存、N+1 与插件机制，并给出大数据量读写和常见踩坑的解法 MyBatis 的面试重点不是会写 CRUD，而是理解它如何在「手写 SQL」和「对象映射」之间做取舍，以及这些设计在事务、缓存和批量场景下的副作用。",{"id":6268,"title":3650,"titles":6269,"content":6270,"level":614},"\u002Fjava-interview\u002Fmybatis#_1-和-有什么区别",[3227],"#{} 会被编译成 PreparedStatement 的占位符 ?，参数通过 TypeHandler 绑定，能防止 SQL 注入，也能复用执行计划，应该作为传值的默认选择。 ${} 是字符串直接替换，发生在 SQL 解析之前。它适合表名、列名、排序字段这类无法用占位符表达的位置，但必须对输入做白名单校验，否则就是注入漏洞。 一个常见误区是「#{} 会加引号，${} 不会」——本质区别是预编译绑定与文本拼接，引号只是表象。order by #{column} 会变成 order by 'name'，排序不生效，正是这个原因。 另外，可空参数在部分数据库（如 Oracle）需要显式 jdbcType，否则可能报无效列类型；MySQL 通常不强制要求。",{"id":6272,"title":3291,"titles":6273,"content":6274,"level":614},"\u002Fjava-interview\u002Fmybatis#_2-mapper-接口没有实现类为什么能调用",[3227],"启动时 MyBatis 扫描 Mapper 接口，把每个方法解析成 MappedStatement，以「命名空间 + 方法名」为 key 存入 Configuration。 调用时通过 MapperProxyFactory 生成 JDK 动态代理。MapperProxy 拦截方法调用，根据方法签名找到对应语句，转换成 SqlSession.selectOne、selectList、insert 等操作。 所以接口只是「声明」，SQL 的真正来源是 XML 或注解。方法重载在 MyBatis 中通常不被支持，因为语句 id 只由方法名决定；参数名在编译时若没保留，需要用 @Param 显式命名。 理解这一点也能解释为什么 Mapper 不能被随意 new：它依赖 SqlSession 和配置上下文，脱离容器创建出来的代理无法工作。",{"id":6276,"title":3343,"titles":6277,"content":6278,"level":614},"\u002Fjava-interview\u002Fmybatis#_3-一级缓存和二级缓存是怎么回事生产环境要不要开",[3227],"一级缓存是 SqlSession 级别的，默认开启，底层是一个 PerpetualCache。同一会话中相同的语句和参数会命中缓存，执行增删改或提交、回滚后失效。localCacheScope=STATEMENT 可以让它只在单条语句内生效。 与 Spring 集成后，如果方法没有事务，每次 Mapper 调用都可能使用新的 SqlSession，一级缓存几乎不命中；处于同一事务时才会共享。 二级缓存是命名空间级别，需要显式配置 \u003Ccache\u002F>，跨会话共享，要求结果可序列化。它在多节点部署下容易读到陈旧数据，写操作也只清空当前命名空间的缓存，因此生产环境通常建议关闭，改用 Redis 等集中式缓存。",{"id":6280,"title":3654,"titles":6281,"content":6282,"level":614},"\u002Fjava-interview\u002Fmybatis#_4-resultmap-和-n1-问题怎么处理",[3227],"resultMap 负责列到属性的映射，支持 association（一对一）和 collection（一对多）。它能处理下划线转驼峰、类型转换和复杂嵌套，比 resultType 的自动映射更可控。 嵌套结果有两种写法：一种是 join 一次查回，用 resultMap 的 id 标签合并重复行；另一种是「嵌套查询」，主查询后再按 id 发子查询，配置 fetchType=\"lazy\" 可以延迟加载。 后者就是 N+1 的来源：查 100 条订单会再发 100 次明细查询。解决思路是 join 一次查回，或先收集主键再用 IN 批量查、在内存里组装。延迟加载只能推迟开销，不能消除次数。 还要注意 join 一对多时结果集行数会膨胀，分页要谨慎，先分页主表再查明细通常更稳。",{"id":6284,"title":3419,"titles":6285,"content":6286,"level":614},"\u002Fjava-interview\u002Fmybatis#_5-插件机制是怎么实现的分页插件原理是什么",[3227],"MyBatis 允许拦截四类对象的方法：Executor、StatementHandler、ParameterHandler、ResultSetHandler。插件通过 @Intercepts 和 @Signature 声明拦截点，再由 InterceptorChain 逐层包装成动态代理。 多个插件形成责任链，顺序由配置决定；包装的是同一批核心对象，所以顺序会影响 SQL 改写和参数处理的结果。 分页插件通常在 Executor.query 处拦截：先根据原 SQL 生成 count 语句，再改写为带 limit 的语句，最后把结果包装成分页对象。有的实现依赖方言识别，复杂 SQL 的 count 优化需要谨慎。 使用分页插件时要注意：分页参数常放在 ThreadLocal 中，必须「设置后立即查询」，中间插入其他查询会串页；异步线程里也不会自动传递。",{"id":6288,"title":3474,"titles":6289,"content":6290,"level":614},"\u002Fjava-interview\u002Fmybatis#_6-大批量写入和超大结果集怎么处理",[3227],"批量写入有三条路：在 foreach 中拼多值 insert；使用 ExecutorType.BATCH 并定期 flushStatements；或交给上层做分批提交。MySQL 连接串加 rewriteBatchedStatements=true 才能把批量语句真正合并发送。 批处理要注意事务大小：一次提交几十万行会撑大 undo、拉长锁持有时间。按几百到几千条分批，并评估失败重试的幂等性。 超大结果集不要用 selectList 一次装进内存。可以用 ResultHandler 逐行处理，或用 Cursor 流式读取，同时把 fetchSize 设为合适的值；MySQL 驱动需要流式模式才真正逐行拉取，连接不能被其他查询复用。",{"id":6292,"title":3521,"titles":6293,"content":6294,"level":614},"\u002Fjava-interview\u002Fmybatis#_7-mybatis-和-jpa-hibernate-怎么选",[3227],"MyBatis 把 SQL 控制权交给开发者，适合复杂查询、遗留库、需要精细优化的场景；代价是重复的映射和手写 SQL，简单 CRUD 显得啰嗦。 JPA\u002FHibernate 以对象模型为中心，自动生成 SQL，开发效率高，但复杂查询容易失控，懒加载和缓存的行为需要经验才能驾驭。 实际项目里常见混合方案：核心写操作和复杂报表用 MyBatis，后台管理等简单 CRUD 用 JPA 或 MyBatis-Plus。MyBatis-Plus 在 MyBatis 之上提供通用 CRUD、条件构造器和分页，但复杂 SQL 仍要手写，别把它当成完全的 ORM。 选型的关键不是「哪个更先进」，而是团队能否为生成出来的 SQL 负责。",{"id":6296,"title":3537,"titles":6297,"content":6298,"level":614},"\u002Fjava-interview\u002Fmybatis#_8-日常使用中有哪些容易踩的坑",[3227],"参数名：Spring Boot 编译默认保留参数名，但依赖构建配置；多参数方法建议显式 @Param，避免升级 JDK 或改编译参数后失效。 映射：开启 mapUnderscoreToCamelCase 后仍要注意别名覆盖；resultType 与 resultMap 不要混用；枚举、JSON 字段需要自定义 TypeHandler。 查询：selectOne 返回多行会抛 TooManyResultsException；\u003Cif test=\"flag == 'Y'\"> 在 OGNL 中比较的是字符，字符串判断应写成 \u003Cif test='flag == \"Y\"'>，或使用 'Y'.toString().equals(flag) 这类明确写法。 事务与连接：批处理、流式查询会长时间占用连接；多数据源要确认事务管理器和 SqlSessionFactory 是否配对；allowMultiQueries 打开后要评估注入风险。",{"id":6300,"title":501,"titles":6301,"content":6302,"level":614},"\u002Fjava-interview\u002Fmybatis#易错点与自测",[3227],"${} 不是「不加引号的 #{}」，而是文本替换，任何来自用户的输入都不能直接拼。 二级缓存是命名空间级、跨会话的，分布式下默认不要开。 延迟加载解决的是「何时查」，不是「查多少次」，N+1 仍要改成批量或 join。 自测：① 描述一次 Mapper 方法调用从代理到 JDBC 的完整链路；② 写出批量插入的两种实现并说明事务边界；③ 解释为什么分页插件在异步线程里会失效。",{"id":6304,"title":577,"titles":6305,"content":6306,"level":614},"\u002Fjava-interview\u002Fmybatis#参考资料",[3227],"MyBatis 3 官方文档 — 动态 SQL、resultMap、缓存与插件 MyBatis 源码（GitHub） — MapperProxy、Executor 与拦截链实现 MySQL Connector\u002FJ 配置 — rewriteBatchedStatements 与流式结果集参数",{"id":4137,"title":3671,"titles":6308,"content":6309,"level":1206},[],"以 InnoDB 8.0 为基线，讲清聚簇索引、索引失效、MVCC 与锁，并给出 EXPLAIN、深分页和大表变更的实战方法 MySQL 题如果只答「加索引」，很难拿到高分。面试官更想听的是：为什么这条 SQL 用不上索引、这条更新加了什么锁、RR 到底解决了什么又没解决什么。以下默认 InnoDB、MySQL 8.0。 场景 关键机制 常见误判 按主键查一行 聚簇索引，叶子即行数据 以为二级索引也要回表 按普通索引查整行 二级索引 → 主键 → 回表 忽略回表成本 查询列都在索引里 覆盖索引，免回表 以为必须 SELECT * 范围条件 + 索引过滤 索引下推（ICP） 以为过滤一定回表后做 高并发读写同一行 行锁 + 一致性读 以为 RR 下读都不加锁",{"id":6311,"title":3755,"titles":6312,"content":6313,"level":614},"\u002Fjava-interview\u002Fmysql#_1-innodb-的索引结构是什么聚簇索引和二级索引有什么区别",[3671],"InnoDB 用 B+ 树组织索引。主键索引是聚簇索引，叶子节点直接存放整行数据，所以按主键查最快；如果没有主键，会选第一个非空唯一索引，再没有就生成隐藏的行 ID。 二级索引的叶子存放的是索引列加主键值。查整行时需要先查二级索引拿到主键，再回聚簇索引取数据，这个动作叫回表，也是「索引很多但查询仍然慢」的常见原因。 B+ 树的非叶子节点只存键和指针，扇出大、层数少，通常三到四层就能覆盖千万级数据。页大小默认 16KB，顺序主键插入能减少页分裂，随机 UUID 主键则会带来更多分裂和碎片。 MyISAM 的索引和数据分离，索引叶子存的是行地址，没有聚簇概念，也不支持事务和行锁。",{"id":6315,"title":3771,"titles":6316,"content":6317,"level":614},"\u002Fjava-interview\u002Fmysql#_2-哪些写法会导致索引失效",[3671],"最常见的是在索引列上做函数或运算，例如 WHERE DATE(create_time) = '2026-10-01'、WHERE id + 1 = 10。应改成范围条件，或建立函数索引（MySQL 8.0.13 起支持）。 隐式类型转换也会失效：字符串列与数字比较时，MySQL 会把列转成数字，索引无法使用。参数类型要和列定义一致。 其他情况包括：LIKE '%关键字' 前导通配；复合索引不满足最左前缀；OR 两侧有一侧没有索引；!=、NOT IN 在选择性差时被优化器放弃；对索引列使用 IS NULL 并非一定失效，要看数据分布。 最后要理解「失效」很多时候是优化器的成本选择：当回表行数占比很高，全表扫描反而更便宜，强制走索引未必更快。",{"id":6319,"title":3813,"titles":6320,"content":6321,"level":614},"\u002Fjava-interview\u002Fmysql#_3-什么是覆盖索引和索引下推",[3671],"覆盖索引指查询需要的列全部能在索引里拿到，不需要回表。EXPLAIN 的 Extra 会显示 Using index。 例如给 (user_id, status, created_at) 建联合索引，查询 SELECT status, created_at FROM orders WHERE user_id = ? 就能覆盖；如果再加 SELECT *，就得回表。 索引下推（ICP）是在存储引擎层用索引中的列提前过滤，减少回表次数。例如联合索引 (name, age)，条件 WHERE name LIKE '张%' AND age = 20，age 可以在索引上先判断，Extra 显示 Using index condition。 覆盖索引和索引下推解决的是不同问题：前者省掉回表，后者减少回表。设计联合索引时，把等值条件放前面、范围条件放后面，通常能同时兼顾过滤和排序。",{"id":6323,"title":3864,"titles":6324,"content":6325,"level":614},"\u002Fjava-interview\u002Fmysql#_4-mvcc-是怎么实现的rr-能完全解决幻读吗",[3671],"InnoDB 每行有隐藏的事务 ID 和回滚指针，回滚指针把旧版本串成 undo 链。读取时根据 ReadView 判断哪个版本可见：当前事务 ID 小于最小活跃事务 ID 的版本可见，大于等于最大事务 ID 的不可见，处于活跃列表中的也不可见。 RC 每条语句生成一个新的 ReadView，所以能读到别人已提交的修改；RR 在事务第一次快照读时生成，之后复用，因此同一事务内多次查询结果一致。 但 RR 不能在所有场景下消除幻读：快照读靠一致性视图，当前读（SELECT ... FOR UPDATE、UPDATE、DELETE）会读最新版本并加锁。如果先快照读、再当前读，或者两个事务以不同顺序读写，仍可能看到「幻影」。 间隙锁和 next-key lock 是 RR 下当前读防幻读的手段，代价是更大的锁范围和更高的死锁概率。",{"id":6327,"title":3889,"titles":6328,"content":6329,"level":614},"\u002Fjava-interview\u002Fmysql#_5-innodb-有哪些锁间隙锁和-next-key-lock-怎么用",[3671],"按粒度分表锁、行锁和意向锁；按模式分共享锁和排他锁。行锁实际加在索引记录上，所以「没有索引的更新」可能退化成锁全表。 记录锁锁定单条索引记录；间隙锁锁定两条记录之间的开区间；next-key lock 是记录锁加前面的间隙锁。插入时还会有插入意向锁，用来表示「准备插入这个间隙」，它与间隙锁冲突。 RC 隔离级别下基本只用记录锁；RR 下范围查询会使用 next-key lock。这也是为什么线上常用 RC：锁范围更小、死锁更少，代价是同一事务内可能出现不可重复读。 死锁排查看 SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK，MySQL 8 还可以查 performance_schema.data_lock_waits。降低死锁的关键是短事务、固定加锁顺序、WHERE 条件走索引。",{"id":6331,"title":4129,"titles":6332,"content":6333,"level":614},"\u002Fjava-interview\u002Fmysql#_6-explain-重点看哪些列慢-sql-怎么优化",[3671],"先看 type：const、eq_ref、ref、range 较好，index 是全索引扫描，ALL 是全表扫描。再看 key 是否命中预期索引、key_len 是否符合联合索引的前缀长度、rows 和 filtered 估算的扫描量。 Extra 信息量很大：Using index 是覆盖索引；Using index condition 是索引下推；Using where 表示还需要在服务层过滤；Using temporary 和 Using filesort 往往意味着排序或分组没有用上索引。 优化顺序通常是：确认 SQL 语义和返回量 → 补或改索引 → 重写查询（避免函数、拆 OR、减少回表）→ 调整分页和批量大小 → 最后才考虑优化器提示。MySQL 8.0.18 起可以用 EXPLAIN ANALYZE 看实际执行耗时。 不要迷信 FORCE INDEX，它会把优化器的判断固化成风险；统计信息不准时先 ANALYZE TABLE。",{"id":6335,"title":4003,"titles":6336,"content":6337,"level":614},"\u002Fjava-interview\u002Fmysql#_7-深分页为什么慢怎么优化",[3671],"LIMIT 1000000, 20 需要先扫描并丢弃前 100 万行，再取 20 行，扫描量和偏移量成正比。即使命中索引，也要付出大量回表或遍历成本。 第一种方案是延迟关联：先在覆盖索引上查出主键，再关联原表取数据。子查询只扫索引，回表只发生在最终 20 行。 SELECT o.*\nFROM orders o\nJOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 20) t ON o.id = t.id; 第二种是游标分页：记住上一页最后一条的排序键，下一页用 WHERE id > ? ORDER BY id LIMIT 20。它稳定且高效，但只能顺序翻页，不适合随机跳页。 如果业务确实需要跳页，考虑限制最大页数、改为搜索条件缩小范围，或用搜索引擎\u002F数仓承接这类查询。",{"id":6339,"title":4048,"titles":6340,"content":6341,"level":614},"\u002Fjava-interview\u002Fmysql#_8-大表-ddl-和分库分表要注意什么",[3671],"MySQL 8 的 DDL 分 INSTANT、INPLACE 和 COPY 三类：加列、改默认值等在满足条件时可以秒级完成；改字符集、重建索引往往要 COPY 或 INPLACE，期间仍会消耗大量 IO。即使算法支持在线，也要先确认是否持锁、是否影响主从延迟。 执行前检查长事务和未提交事务，避免拿不到元数据锁导致后续请求排队。大表变更常用 gh-ost 或 pt-online-schema-change 做影子表切换，并安排低峰期和回滚预案。 分库分表不是第一选择：先考虑索引优化、归档冷数据、读写分离和分区表。确需拆分时，分片键要兼顾均匀性和查询命中率；跨库 join、分页、聚合和事务都会变复杂，通常要配套全局 ID、数据同步和路由层。 扩容是分库分表最容易低估的部分，提前设计好双写、迁移和校验方案，比事后补救成本低得多。",{"id":6343,"title":501,"titles":6344,"content":6345,"level":614},"\u002Fjava-interview\u002Fmysql#易错点与自测",[3671],"「建了索引就一定走」和「走了索引就一定快」都不对，要看回表量和选择性。 RR 的「可重复读」只对快照读成立，当前读看到的是最新已提交数据。 间隙锁只在 RR 及以上、且涉及范围条件时明显，RC 下不要照搬 RR 的锁分析。 自测：① 给出一个联合索引并解释最左前缀；② 用 ReadView 的四个字段判断某个版本是否可见；③ 写出深分页的延迟关联写法并说明为什么更快。",{"id":6347,"title":577,"titles":6348,"content":6349,"level":614},"\u002Fjava-interview\u002Fmysql#参考资料",[3671],"MySQL 8.4 参考手册：InnoDB 锁 — 记录锁、间隙锁与 next-key lock MySQL 8.4 参考手册：InnoDB 多版本并发控制 — 一致性读与隔离级别 MySQL 8.4 参考手册：EXPLAIN 输出 — type、key、Extra 各列含义 MySQL 8.4 参考手册：在线 DDL — INSTANT\u002FINPLACE\u002FCOPY 的边界 html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"id":4442,"title":4144,"titles":6351,"content":6352,"level":1206},[],"从握手挥手、可靠传输到 HTTP\u002F2、HTTPS 和 Netty 的线程模型，梳理网络面试的高频考点与边界 网络题看起来零散，其实可以围绕「连接怎么建立、数据怎么可靠送达、协议怎么演进、框架怎么封装」四条线来准备。",{"id":6354,"title":4153,"titles":6355,"content":6356,"level":614},"\u002Fjava-interview\u002Fnetworking#_1-tcp-为什么是三次握手四次挥手",[4144],"三次握手：客户端发 SYN，服务端回 SYN-ACK，客户端再回 ACK。两次不够，是因为服务端无法确认客户端收到了自己的 SYN；同时也能防止网络中滞留的旧连接请求突然到达，造成错误连接。第三次 ACK 让双方都确认「收发能力正常、初始序列号已同步」。 四次挥手是因为 TCP 是全双工的：主动方发 FIN 只表示「我没有数据要发了」，对方还可以继续发送，所以要先回 ACK，等自己的数据发完再发 FIN。主动方收到对方的 FIN 后回 ACK，连接才完全关闭。 主动关闭方会进入 TIME_WAIT，等待 2MSL。目的有两个：保证最后一个 ACK 能到达对方，否则对方会重发 FIN；让本连接中的旧报文在网络中消失，避免影响使用相同四元组的新连接。 高并发短连接的服务端容易出现大量 TIME_WAIT，解决方向是连接复用、长连接，而不是盲目调小等待时间。",{"id":6358,"title":4430,"titles":6359,"content":6360,"level":614},"\u002Fjava-interview\u002Fnetworking#_2-time_wait-和-close_wait-分别意味着什么",[4144],"TIME_WAIT 出现在主动关闭方，是正常的收尾状态，通常持续 2MSL。数量很大往往说明短连接太多，或者服务端主动关闭了连接；优先考虑连接池和 keep-alive。 CLOSE_WAIT 出现在被动关闭方，表示「对方已经发了 FIN，但本端应用还没有调用 close」。大量 CLOSE_WAIT 几乎总是代码问题：连接没有在 finally 中关闭、异常路径漏了释放、连接池配置不当。 排查时用 ss -tan state close-wait 或 netstat 看数量，再结合进程和线程栈定位是哪段代码持有连接。CLOSE_WAIT 不会自己消失，必须由应用关闭。 还要区分 ESTABLISHED 过多（并发高或泄漏）和 SYN_RECV 过多（可能的 SYN Flood）。",{"id":6362,"title":4222,"titles":6363,"content":6364,"level":614},"\u002Fjava-interview\u002Fnetworking#_3-tcp-如何保证可靠传输",[4144],"每个字节都有序号，接收方用累积确认告诉发送方「我已经收到了哪里」。超时未确认会重传；收到三个重复 ACK 时触发快速重传，不必等到超时。校验和用于发现传输错误。 滑动窗口做流量控制：接收方通过窗口字段告诉发送方还能接收多少，避免发送过快压垮接收端。窗口扩大选项让高带宽高延迟链路也能有足够吞吐。 拥塞控制则根据网络状况调整发送速率，包括慢启动、拥塞避免、快速恢复等阶段。现代内核常用 CUBIC，部分场景用 BBR，它们对丢包和延迟的假设不同。 TCP 保证的是「可靠、有序的字节流」，不保证应用层消息边界，也不保证实时性。UDP 不建立连接、不重传，适合实时音视频、DNS 这类能容忍少量丢包的场景。",{"id":6366,"title":4238,"titles":6367,"content":6368,"level":614},"\u002Fjava-interview\u002Fnetworking#_4-粘包拆包是怎么产生的怎么解决",[4144],"TCP 是字节流，没有消息边界。发送方两次 write 的数据可能被合并成一个包到达，也可能一条消息被拆成多个包，这就是「粘包」和「拆包」。UDP 有数据报边界，不存在这个问题，但会丢包和乱序。 解决办法是在应用层定义边界：定长消息、分隔符、或者最常用的「长度字段 + 消息体」。长度字段方案最稳妥，能处理任意二进制内容，也是大多数 RPC 协议的选择。 Netty 提供了现成的解码器：LengthFieldBasedFrameDecoder 处理长度前缀，DelimiterBasedFrameDecoder 处理分隔符，FixedLengthFrameDecoder 处理定长。还要配合 IdleStateHandler 处理空闲连接。 注意长度字段的字节序、最大长度限制和半包缓存，恶意超长长度可能造成内存耗尽。",{"id":6370,"title":4274,"titles":6371,"content":6372,"level":614},"\u002Fjava-interview\u002Fnetworking#_5-http11http2http3-有什么区别",[4144],"HTTP\u002F1.1 支持持久连接，但一个连接同一时刻通常只处理一个请求，浏览器靠开多个连接提升并发；管道化理论上可以连续发请求，但因队头阻塞和代理兼容问题很少使用。 HTTP\u002F2 引入二进制分帧和多路复用，一个 TCP 连接上可以并行多个流，头部用 HPACK 压缩，减少了连接数和头部开销。但 TCP 层仍有队头阻塞：一个包丢失，所有流都要等重传。 HTTP\u002F3 基于 QUIC（UDP），流之间独立，丢包只影响所在流；内置 TLS 1.3，支持 0-RTT 恢复，连接迁移也更平滑。代价是 UDP 在部分网络中被限制，运维和排查方式与 TCP 不同。 选型时还要考虑中间设备和服务端支持，HTTP\u002F2 的收益在高延迟、多资源场景最明显。",{"id":6374,"title":4290,"titles":6375,"content":6376,"level":614},"\u002Fjava-interview\u002Fnetworking#_6-https-的握手过程是怎样的",[4144],"客户端先发 ClientHello，包含支持的协议版本、加密套件和随机数；服务端回 ServerHello 和证书链。客户端验证证书：是否由受信任 CA 签发、域名是否匹配、是否在有效期内、是否被吊销。 验证通过后，双方通过密钥交换算法协商出共享密钥。现代 TLS 使用 ECDHE，具备前向安全：即使服务器私钥泄露，也无法解密过去的会话。之后用对称加密传输数据，因为对称加密性能更好。 TLS 1.3 把握手压缩到 1-RTT，并支持会话恢复时的 0-RTT，但 0-RTT 数据存在重放风险，只适合幂等请求。 中间人攻击的前提是客户端信任了攻击者的证书，所以证书校验、HSTS 和证书透明度都很重要。HTTPS 保护传输，不保护服务端被入侵后的数据。",{"id":6378,"title":4306,"titles":6379,"content":6380,"level":614},"\u002Fjava-interview\u002Fnetworking#_7-cookiesessiontokenjwt-怎么选",[4144],"Cookie 是浏览器存储机制，HttpOnly 防止脚本读取，Secure 只走 HTTPS，SameSite 缓解 CSRF。Session 把状态放在服务端，浏览器只存 session id，撤销方便，但需要共享存储。 Token 是广义的凭证，JWT 是其中一种自包含格式：头部、载荷、签名。服务端验签即可，不必查存储，适合水平扩展；但载荷是明文可读的，不能放敏感信息，撤销也需要额外机制。 常见组合是「短期 access token + 长期 refresh token」，敏感操作再校验权限或黑名单。把 JWT 放在 localStorage 容易受 XSS 影响，放在 HttpOnly Cookie 又要防 CSRF，需要配套措施。 选择的核心是状态放在哪、撤销要求多强、是否有跨域和跨端需求，而不是「JWT 更现代」。",{"id":6382,"title":4341,"titles":6383,"content":6384,"level":614},"\u002Fjava-interview\u002Fnetworking#_8-netty-为什么高性能零拷贝是什么",[4144],"Netty 用主从 Reactor 模型：boss 线程组负责接收连接，worker 线程组负责读写事件；每个 Channel 绑定一个 EventLoop，事件在单线程内串行处理，避免锁竞争。 它基于 NIO 多路复用，支持海量连接；ByteBuf 用读指针和写指针分离读写位置，支持池化和堆外内存，减少分配与复制；Pipeline 用责任链组织编解码和业务处理，扩展方便。 「零拷贝」在不同语境下含义不同：操作系统层面是 sendfile 让数据不经过用户态；Netty 层面更多是 CompositeByteBuf 组合多个缓冲区、FileRegion 传输文件、以及用堆外内存减少 JVM 堆与内核之间的复制。它不等于「完全不拷贝」。 使用时要防止在 EventLoop 里执行阻塞操作，否则会卡住该线程上的所有连接；还要注意 ByteBuf 的引用计数和泄漏检测，堆外内存泄漏不会体现在堆 dump 里。",{"id":6386,"title":501,"titles":6387,"content":6388,"level":614},"\u002Fjava-interview\u002Fnetworking#易错点与自测",[4144],"三次握手不是为了「确认双方在线」这么简单，核心是序列号同步和防止旧连接。 TIME_WAIT 是主动关闭方的正常状态，CLOSE_WAIT 堆积多半是应用没关连接。 HTTP\u002F2 解决了 HTTP 层队头阻塞，没有解决 TCP 层；HTTP\u002F3 才换掉 TCP。 自测：① 解释 2MSL 的两个作用；② 说明长度字段解码器如何处理半包；③ 区分 sendfile 零拷贝与 Netty 的缓冲区组合。",{"id":6390,"title":577,"titles":6391,"content":6392,"level":614},"\u002Fjava-interview\u002Fnetworking#参考资料",[4144],"RFC 9293：TCP — 连接管理、重传与流量控制的规范 RFC 9110：HTTP 语义 — 方法、状态码与连接语义 RFC 9000：QUIC — HTTP\u002F3 的传输基础 Netty 4.x 用户指南 — Reactor 模型、Pipeline 与编解码器",{"id":4908,"title":4449,"titles":6394,"content":6395,"level":1206},[],"梳理进程线程协程、虚拟内存、epoll 与 CPU\u002F内存\u002F磁盘排查方法，并补上容器环境下 JVM 的资源边界 Java 后端的操作系统题通常带着「线上」两个字：不是让你背内核源码，而是能不能用 top、vmstat、iostat 解释现象，并知道 JVM 与容器的边界。 现象 先看什么 常见原因 CPU 高 top、top -Hp、线程栈 死循环、GC、正则回溯、加密计算 负载高但 CPU 不高 vmstat 的 r 和 b 列 IO 等待、不可中断睡眠 内存涨 free、RSS、cgroup 指标 堆外内存、page cache、线程栈 磁盘慢 iostat -x 的 await、%util 随机 IO、fsync 频繁、磁盘打满",{"id":6397,"title":4553,"titles":6398,"content":6399,"level":614},"\u002Fjava-interview\u002Foperating-systems#_1-进程线程协程有什么区别",[4449],"进程是资源分配的基本单位，拥有独立地址空间、文件描述符和信号处理；线程是调度的基本单位，同一进程内的线程共享堆和方法区，各自有栈和寄存器。 线程切换要进入内核，保存和恢复上下文，还可能刷新 TLB、影响缓存局部性，成本通常是微秒级。进程切换还要切换地址空间，开销更大。 协程是用户态的轻量执行单元，由运行时调度，切换不经过内核，成本低。它适合大量阻塞在 IO 上的任务，但不等于「免费」：阻塞系统调用仍会占用底层线程，CPU 密集任务也不会因此变快。 Java 21 的虚拟线程由 JVM 调度到载体线程上，遇到阻塞 IO 会自动让出，本质上是把协程模型引入 JVM。注意它不能突破数据库连接池、下游限流等外部瓶颈。",{"id":6401,"title":4569,"titles":6402,"content":6403,"level":614},"\u002Fjava-interview\u002Foperating-systems#_2-虚拟内存是怎么工作的",[4449],"每个进程看到的是独立的虚拟地址空间，通过页表映射到物理内存。CPU 访问地址时由 MMU 查表，TLB 缓存最近的映射，避免每次都走多级页表。 内存按页管理，通常 4KB。访问未映射或未加载的页会触发缺页中断：如果是合法映射但数据不在内存，就从磁盘或 swap 调入；如果是非法访问，进程收到段错误。 这种按需加载让进程可以申请超过物理内存的地址空间，也让多个进程共享同一份文件页。文件读写默认走 page cache，写操作先改缓存，变成脏页后由内核回写。 问题在于，free 显示的可用内存很少不一定代表内存不足，page cache 会在压力下回收。真正要关注的是可用内存、swap 使用和 OOM 事件。",{"id":6405,"title":4588,"titles":6406,"content":6407,"level":614},"\u002Fjava-interview\u002Foperating-systems#_3-五种-io-模型怎么理解epoll-为什么高效",[4449],"阻塞 IO 在等待数据和拷贝数据两个阶段都阻塞；非阻塞 IO 轮询数据是否就绪，就绪后拷贝仍阻塞；IO 多路复用在单个线程里等待多个 fd 就绪；信号驱动在就绪时收到信号；异步 IO 连数据拷贝完成后再通知。 select 用位图传递 fd，有数量上限，返回后要遍历全部 fd；poll 用数组去掉了上限，但仍需遍历。epoll 把 fd 注册到内核的红黑树，就绪的 fd 放入就绪链表，调用时只返回就绪项。 epoll 支持水平触发和边缘触发。水平触发只要还有数据就会持续通知，编程简单；边缘触发只在状态变化时通知一次，必须一次读到 EAGAIN，通常配合非阻塞 fd，效率更高但更容易写错。 Reactor 模式就是把 epoll 的事件循环与业务处理解耦，Netty、Redis、Nginx 都是这个思路。",{"id":6409,"title":4608,"titles":6410,"content":6411,"level":614},"\u002Fjava-interview\u002Foperating-systems#_4-cpu-使用率高怎么排查",[4449],"先用 top 看是用户态还是内核态高：us 高多半是业务计算、GC 或死循环；sy 高可能是系统调用、锁竞争或上下文切换过多。 找到进程后，top -Hp \u003Cpid> 定位到具体线程，把线程 id 转成十六进制，再在 jstack 输出里按 nid 搜索，就能看到对应栈。Java 进程还可以用 JFR 或 async-profiler 生成火焰图，直接看热点方法。 GC 也会表现为 CPU 高：用 jstat -gcutil 看回收频率和耗时，若 Full GC 频繁，问题在内存而不是计算。 负载高但 CPU 不高时，看 vmstat 的 b 列和 iostat 的 await，可能是 IO 阻塞或大量不可中断睡眠。别只盯着 CPU 使用率一个指标。",{"id":6413,"title":4660,"titles":6414,"content":6415,"level":614},"\u002Fjava-interview\u002Foperating-systems#_5-内存不足时系统会发生什么",[4449],"当物理内存和可回收的 page cache 都不足时，内核会尝试回收、压缩，必要时使用 swap。swap 频繁换入换出会导致系统抖动，延迟急剧上升。 如果仍然无法满足，Linux 的 OOM Killer 会按评分选择进程杀掉，容器里则可能表现为 OOMKilled、退出码 137。这类问题不会在 Java 堆 dump 里留下明显证据，要看 dmesg 和 cgroup 的内存事件。 Java 进程的内存不只是堆：还有元空间、直接内存、线程栈、JIT 代码缓存和本地库。容器内存限制下，堆要预留这些空间，不能简单地把 -Xmx 设成容器上限。 排查用 free -h、\u002Fproc\u002Fmeminfo、smem、jcmd VM.native_memory，以及容器的 memory.current 和 memory.max。",{"id":6417,"title":4706,"titles":6418,"content":6419,"level":614},"\u002Fjava-interview\u002Foperating-systems#_6-磁盘-io-慢怎么定位",[4449],"先看 iostat -x 1：r\u002Fs、w\u002Fs 是 IOPS，rkB\u002Fs、wkB\u002Fs 是吞吐，await 是平均等待时间，%util 表示设备忙碌程度，aqu-sz 反映排队深度。 await 高而 %util 低，可能是单次 IO 很大或底层存储延迟高；%util 接近 100% 且队列长，说明设备已经饱和。 再看是顺序还是随机：数据库和日志写入的随机 IO 对机械盘很不友好，SSD 的随机性能好得多。文件系统、RAID 和云盘的 IOPS 上限也要纳入考虑。 应用层要区分「读写量大」和「fsync 频繁」。每次写都同步刷盘会放大延迟，批量提交和合理的刷盘策略往往比换硬件更有效。",{"id":6421,"title":4756,"titles":6422,"content":6423,"level":614},"\u002Fjava-interview\u002Foperating-systems#_7-死锁的四个条件和处理方式",[4449],"死锁需要互斥、持有并等待、不可抢占、循环等待同时成立。破坏任意一个就能避免，但工程上最常用的是固定加锁顺序和超时重试。 不可抢占意味着已经拿到的锁不能被强制收回，所以「按顺序申请，按逆序释放」能消除循环等待；tryLock 带超时可以让线程在拿不到锁时退出，避免无限等待。 检测方面，Java 用 jcmd Thread.print 或 jstack，输出会标出相互等待的线程；数据库用 SHOW ENGINE INNODB STATUS；操作系统层面可以看 \u002Fproc\u002F\u003Cpid>\u002Fstack 和等待链。 还要区分死锁、活锁和饥饿：活锁是线程一直在重试但无法推进，饥饿是低优先级线程长期拿不到资源。",{"id":6425,"title":4788,"titles":6426,"content":6427,"level":614},"\u002Fjava-interview\u002Foperating-systems#_8-容器里的-jvm-要注意什么",[4449],"容器用 namespace 做隔离、cgroup 做资源限制。JDK 10 起 UseContainerSupport 默认开启，JVM 会读取 cgroup 的 CPU 和内存限制，而不是宿主机总量；更早的版本会误判，必须显式设置。 内存方面，堆上限可以用 -XX:MaxRAMPercentage 按容器内存比例设置，但要给元空间、直接内存、线程栈和 JIT 留出空间。设置过满时，容器 OOM Killer 会直接杀掉进程，而不是抛 Java 堆 OOM。 CPU 方面，-XX:ActiveProcessorCount 会影响 GC 线程数和默认并行度；cpu.max 的配额可能导致 JVM 看到的核数与实际可用算力不一致，线程池要按实际配额压测。 还要注意 cgroup v1 与 v2 的指标路径不同，监控采集和 JVM 版本都要确认支持。",{"id":6429,"title":501,"titles":6430,"content":6431,"level":614},"\u002Fjava-interview\u002Foperating-systems#易错点与自测",[4449],"协程不是「更快的线程」，它优化的是阻塞等待的调度成本。 free 少不等于内存不足，page cache 可回收；容器 OOM 与 Java 堆 OOM 是两件事。 epoll 高效的前提是 fd 已注册且事件就绪，边缘触发下必须读到 EAGAIN。 自测：① 说明 await 高但 %util 低的可能原因；② 解释容器内存限制下 -Xmx 为什么要留余量；③ 用 top、printf 和 jstack 串起一次 CPU 高排查。",{"id":6433,"title":577,"titles":6434,"content":6435,"level":614},"\u002Fjava-interview\u002Foperating-systems#参考资料",[4449],"Linux man-pages：epoll — 水平\u002F边缘触发与就绪模型 Linux man-pages：proc — 进程状态、内存与线程信息 Java SE 21：容器感知 — availableProcessors 与容器 CPU 限制 JEP 444：Virtual Threads — 虚拟线程与平台线程的调度差异",{"id":5227,"title":4915,"titles":6437,"content":6438,"level":1206},[],"从底层编码、单线程模型到 RDB\u002FAOF、淘汰策略和分布式锁，系统梳理 Redis 面试题与线上故障的应对方式 Redis 题要避免停留在「有什么数据类型」。真正有区分度的是：为什么这个场景选这个结构、数据丢了最多丢多少、缓存和数据库不一致时怎么办。",{"id":6440,"title":4924,"titles":6441,"content":6442,"level":614},"\u002Fjava-interview\u002Fredis#_1-redis-有哪些数据类型底层编码是什么",[4915],"基础类型有 String、List、Hash、Set、ZSet，扩展类型有 Bitmap、HyperLogLog、GEO 和 Stream。 底层编码会随数据量自动切换：String 有整数编码、短字符串编码和 SDS；Redis 7 起，小 List 直接用 listpack，元素变多后转为 quicklist（由多个 listpack 组成的双向链表）；Hash 和 ZSet 在小数据量下用 listpack，超过阈值后分别转为哈希表和跳表加哈希表；Set 在纯整数时用 intset，否则用 listpack 或哈希表。 选型要贴合访问模式：计数器用 String 的 INCR；排行榜用 ZSet；去重计数用 HyperLogLog（有误差）；签到用 Bitmap；消息队列可以用 Stream，但要评估消费组和持久化语义。 记住一个原则：结构选错，后面靠加机器补不回来。例如用 String 存整个大 JSON 再整体更新，会放大网络和序列化开销。",{"id":6444,"title":4944,"titles":6445,"content":6446,"level":614},"\u002Fjava-interview\u002Fredis#_2-redis-为什么快单线程模型是怎么回事",[4915],"Redis 快在几点：数据在内存；数据结构针对场景优化；用 epoll 做 IO 多路复用；命令执行单线程，避免了锁竞争和上下文切换；协议简单。 「单线程」指命令执行是单线程的。Redis 6 起引入了多线程 IO，用来处理网络读写和协议解析，命令仍在主线程串行执行，所以不需要为数据结构加锁。 代价是任何慢命令都会阻塞所有请求。生产上要避免 KEYS *、大 key 的 HGETALL、对大集合做 SORT、长时间运行的 Lua 脚本，以及直接 DEL 超大 key（用 UNLINK 异步释放）。 排查慢请求可以用 SLOWLOG GET 和 LATENCY DOCTOR，并在监控里关注命令耗时和阻塞型命令的出现频率。",{"id":6448,"title":4987,"titles":6449,"content":6450,"level":614},"\u002Fjava-interview\u002Fredis#_3-rdb-和-aof-怎么选会丢数据吗",[4915],"RDB 是某一时刻的全量快照，BGSAVE 通过 fork 子进程写盘，借助写时复制减少阻塞。文件紧凑、恢复快，但两次快照之间的数据会丢。 AOF 记录写命令，appendfsync 有三个档位：always 最安全但性能最差；everysec 是默认值，最多丢约一秒；no 交给操作系统刷盘，丢得更多。AOF 文件会持续增长，需要重写压缩。 Redis 4 起支持混合持久化：重写时先生成 RDB 格式的基线，再追加增量命令，兼顾恢复速度和数据完整度。 实际选择通常是两者都开：RDB 用于备份和快速恢复，AOF 用于缩小丢失窗口。但无论怎么配，Redis 都不是强一致存储，关键数据必须有数据库或消息队列兜底。",{"id":6452,"title":5023,"titles":6453,"content":6454,"level":614},"\u002Fjava-interview\u002Fredis#_4-过期键是怎么删除的内存满了怎么办",[4915],"过期删除是「惰性 + 定期」组合：访问键时检查是否过期；同时后台周期性随机抽样，把过期比例降到阈值以下。这样避免为每个键建定时器，但也意味着过期键可能短暂占内存。 从节点通常不独立删除过期键，而是等待主节点的删除命令同步，保证主从数据一致。 内存达到 maxmemory 后按淘汰策略处理：noeviction 直接报错；allkeys-lru \u002F allkeys-lfu 在所有键里淘汰；volatile-* 只淘汰设置了过期时间的键；volatile-ttl 优先淘汰剩余时间短的。 LRU 是近似实现，靠采样若干键选最久未用的，maxmemory-samples 越大越精确。LFU 更适合「曾经热、现在冷」的访问分布，但计数器需要衰减。",{"id":6456,"title":5066,"titles":6457,"content":6458,"level":614},"\u002Fjava-interview\u002Fredis#_5-缓存穿透击穿雪崩分别怎么解决",[4915],"穿透是查不存在的数据，请求每次都打到数据库。可以用布隆过滤器提前拦截，或缓存空值并设置较短过期时间；接口层还要做参数校验和限流。 击穿是某个热点 key 过期瞬间，大量请求同时回源。常见做法是互斥重建（只放一个请求查库，其他等待）或逻辑过期（值不过期，异步刷新），前者简单但会阻塞，后者体验好但会读到旧值。 雪崩是大量 key 同时失效，或 Redis 整体不可用。解决思路包括给过期时间加随机抖动、做多级缓存、集群化部署、加熔断降级和限流，以及准备本地兜底数据。 三者容易混：穿透是「数据不存在」，击穿是「单个热点失效」，雪崩是「大面积同时失效或整体故障」。",{"id":6460,"title":5082,"titles":6461,"content":6462,"level":614},"\u002Fjava-interview\u002Fredis#_6-如何保证缓存和数据库的一致性",[4915],"常用的是旁路缓存：读时先查缓存，未命中查库并回填；写时先更新数据库，再删除缓存。 为什么是删除而不是更新缓存：并发更新时，两个写请求的缓存更新顺序可能与数据库提交顺序相反，留下脏数据；删除让下一次读自然回填。 即使先更新库再删缓存，仍有极小概率不一致：读请求在删除前拿到旧值，删除后又把旧值写回。可以用延迟双删、给缓存加版本号，或订阅 binlog 异步删除来收敛。 要接受「最终一致」而不是「强一致」。对一致性要求极高的场景，应让缓存只承载可容忍短暂陈旧的数据，或在读路径上做版本校验。",{"id":6464,"title":5098,"titles":6465,"content":6466,"level":614},"\u002Fjava-interview\u002Fredis#_7-redis-分布式锁怎么做有哪些坑",[4915],"基本写法是 SET lock_key unique_value NX PX 30000：NX 保证互斥，PX 设置过期防止死锁，unique_value 标识持有者。 释放时必须用 Lua 先比较 value 再删除，避免误删别人的锁；业务执行时间可能超过锁租期，需要看门狗续期，Redisson 就封装了这套逻辑。 仍有两个本质问题：主从异步复制下，主节点加锁成功后宕机，锁可能丢失；锁过期与业务完成之间存在时间竞态。前者需要接受「Redis 锁不是强一致」或改用共识系统，后者可以用 fencing token 让下游拒绝旧持有者的写。 所以 Redis 锁适合「防重复、降并发」这类场景，不适合转账扣款这种必须绝对互斥的核心链路。Redlock 也有广泛争议，不要把「多数节点加锁成功」等同于强一致。",{"id":6468,"title":5129,"titles":6469,"content":6470,"level":614},"\u002Fjava-interview\u002Fredis#_8-主从哨兵cluster-有什么区别热-key-和大-key-怎么处理",[4915],"主从解决读扩展和备份，复制是异步的，主节点故障需要人工切换。哨兵在主从之上做监控和自动故障转移，靠多数派投票选主，仍是一个主节点写。Cluster 做分片，16384 个槽分布在多个主节点上，客户端按 CRC16(key) mod 16384 路由，遇到 MOVED \u002F ASK 重定向；多 key 操作要求落在同一槽，可用 hash tag 强制同槽。 热 key 指单个 key 的访问量过大，会打满某个节点。可以做本地缓存、把 key 拆成多个副本分散读、增加只读副本，或在代理层做请求合并。 大 key 指 value 过大或集合元素过多，会造成网络阻塞、删除卡顿和迁移困难。应拆分结构、分页读取、用 UNLINK 删除，并通过 redis-cli --bigkeys 和慢日志定期发现。",{"id":6472,"title":501,"titles":6473,"content":6474,"level":614},"\u002Fjava-interview\u002Fredis#易错点与自测",[4915],"单线程指的是命令执行，不是整个进程只有一个线程。 AOF everysec 仍可能丢约一秒，Redis 不能替代持久化数据库。 缓存一致性只能做到最终一致，方案要配套业务容忍度和监控。 自测：① 说明 ZSet 在小数据量和大数据量下的编码差异；② 比较互斥重建与逻辑过期在击穿场景的优劣；③ 写出分布式锁的加锁与释放命令，并解释 value 为什么必须唯一。",{"id":6476,"title":577,"titles":6477,"content":6478,"level":614},"\u002Fjava-interview\u002Fredis#参考资料",[4915],"Redis 官方文档：数据类型 — 各类型的语义与典型场景 Redis 官方文档：持久化 — RDB、AOF 与混合持久化 Redis 官方文档：分布式锁 — 单实例锁与 Redlock 的说明 Redis 官方文档：客户端缓存 — 多级缓存与失效通知",{"id":5616,"title":5234,"titles":6480,"content":6481,"level":1206},[],"从 IoC 容器、三级缓存到 AOP 代理和事务失效，梳理 Spring 与 Spring Boot 的高频面试链路 Spring 题有一条很自然的链路：容器怎么创建 Bean、怎么给 Bean 加能力、事务为什么依赖代理、Boot 又怎么把这一切自动化。按这条线准备，比孤立背注解有效。以下默认 Spring Framework 6 \u002F Spring Boot 3。",{"id":6483,"title":5602,"titles":6484,"content":6485,"level":614},"\u002Fjava-interview\u002Fspring#_1-怎么理解-ioc-和-dibeanfactory-与-applicationcontext-有什么区别",[5234],"IoC 是控制反转：对象的创建、依赖装配和生命周期交给容器，业务代码只声明需要什么。DI 是它的实现方式，通过构造器、setter 或字段注入把依赖传进来。 BeanFactory 是最基础的容器接口，提供 getBean 和 Bean 生命周期管理，默认懒加载。ApplicationContext 继承它，额外提供资源加载、事件发布、国际化、AOP 集成和注解支持，单例默认在启动时实例化。 实践上优先构造器注入：依赖不可变、便于单元测试、能暴露循环依赖问题。字段注入写法简单，但会让类难以脱离容器测试，也掩盖了职责过多的信号。",{"id":6487,"title":5270,"titles":6488,"content":6489,"level":614},"\u002Fjava-interview\u002Fspring#_2-bean-的生命周期是怎样的",[5234],"大致分四段：实例化、属性填充、初始化、销毁。 实例化通过构造器或工厂方法完成；随后注入依赖并回调各类 Aware 接口，例如 BeanNameAware、ApplicationContextAware。接着执行 BeanPostProcessor 的前置处理，再调用 @PostConstruct、InitializingBean.afterPropertiesSet 和自定义 init-method，最后执行后置处理——AOP 代理通常就在这一步生成。 使用阶段结束后，容器关闭时依次调用 @PreDestroy、DisposableBean.destroy 和 destroy-method。注意原型 Bean 的销毁方法不会由容器调用，需要调用方自己负责。 BeanPostProcessor 是理解 Spring 扩展性的关键：它能在初始化前后改写甚至替换 Bean，@Autowired、@Async、事务代理都依赖这一类扩展点。",{"id":6491,"title":5330,"titles":6492,"content":6493,"level":614},"\u002Fjava-interview\u002Fspring#_3-循环依赖是怎么解决的为什么构造器注入不行",[5234],"单例 Bean 的字段或 setter 循环依赖，靠三级缓存解决：一级放成品 Bean，二级放提前暴露的早期引用，三级放能生成早期引用的工厂。 创建 A 时先实例化并放入三级缓存，填充属性发现依赖 B，转去创建 B；B 依赖 A 时从缓存拿到 A 的早期引用，B 完成后 A 继续填充并最终完成。三级缓存的意义在于：如果 A 需要被 AOP 代理，工厂能在暴露早期引用时就生成代理，而不是暴露原始对象。 构造器注入要求在实例化阶段就拿到依赖，此时对象还不存在，无法提前暴露，因此会抛 BeanCurrentlyInCreationException。Spring Boot 2.6 起默认禁止循环依赖，需要显式开启才恢复旧行为；更好的做法是重构依赖或使用 @Lazy、ObjectProvider 延迟获取。",{"id":6495,"title":5354,"titles":6496,"content":6497,"level":614},"\u002Fjava-interview\u002Fspring#_4-bean-是线程安全的吗单例要注意什么",[5234],"Spring 不保证 Bean 线程安全，它只负责创建和管理。单例 Bean 被所有请求共享，只要存在可变的成员状态，就可能出现并发问题。 无状态的 Service、DAO 天然安全；有状态的对象要么改成方法内局部变量，要么用 ThreadLocal（并记得清理），要么把作用域改成原型。给单例加 synchronized 通常只是把瓶颈集中到一个点上。 常见误区是把 @Scope(\"prototype\") 注到单例里就以为每次调用都会新建：注入只发生一次，单例持有的是同一个原型实例。需要每次获取新对象时应注入 ObjectProvider 或使用作用域代理。",{"id":6499,"title":5380,"titles":6500,"content":6501,"level":614},"\u002Fjava-interview\u002Fspring#_5-spring-aop-的原理是什么为什么自调用会失效",[5234],"Spring AOP 基于动态代理：目标类实现了接口时可用 JDK 代理，否则用 CGLIB 生成子类。Spring Boot 默认 proxyTargetClass = true，即优先 CGLIB，所以 final 类和 final 方法无法被代理。 调用方拿到的是代理对象，代理在方法前后织入通知。所谓自调用失效，是指同类中 this.otherMethod() 绕过了代理，通知自然不会执行；事务、缓存、异步注解都会遇到这个问题。 解决方式：拆到另一个 Bean、注入自身代理、使用 AopContext.currentProxy()（需开启 exposeProxy），或者改用 AspectJ 编译期\u002F加载期织入。生产上更推荐前两种，侵入性小。",{"id":6503,"title":5608,"titles":6504,"content":6505,"level":614},"\u002Fjava-interview\u002Fspring#_6-transactional-在哪些情况下会失效传播行为怎么选",[5234],"失效的常见原因：自调用绕过代理；方法不是 public（Spring 6 起类代理支持 protected 和包可见方法，但仍有边界）；Bean 不是由容器管理；异常被 catch 后没有抛出；抛出的是受检异常而默认只回滚 RuntimeException 和 Error；数据库表本身不支持事务；在另一个线程里执行。 传播行为中，REQUIRED 是默认值，加入现有事务；REQUIRES_NEW 挂起当前事务、独立提交，适合记日志等「必须落库」的操作；NESTED 使用保存点，只对 JDBC 等支持保存点的资源有效。 还要注意只读事务和超时设置，以及事务内不要做远程调用——长事务会长期占用连接和锁。",{"id":6507,"title":5458,"titles":6508,"content":6509,"level":614},"\u002Fjava-interview\u002Fspring#_7-spring-boot-自动配置的原理",[5234],"@SpringBootApplication 组合了配置类、组件扫描和 @EnableAutoConfiguration。后者通过 AutoConfigurationImportSelector 读取候选配置类：Boot 2.7 起使用 META-INF\u002Fspring\u002Forg.springframework.boot.autoconfigure.AutoConfiguration.imports，更早的版本用 spring.factories。 候选类并不会全部生效，而是由条件注解筛选：类路径上是否存在某个类、容器里是否已有同类型 Bean、配置项是否开启。这就是「引入 starter 就自动装配，同时允许自定义覆盖」的机制。 自定义覆盖之所以生效，是因为自动配置类通常带 @ConditionalOnMissingBean，且用户配置先于自动配置处理。排查自动配置问题时，可以开启 debug=true 查看条件评估报告。",{"id":6511,"title":5497,"titles":6512,"content":6513,"level":614},"\u002Fjava-interview\u002Fspring#_8-一个-http-请求在-spring-mvc-里是怎么走完的",[5234],"请求先经过 Servlet 容器的过滤器链，进入 DispatcherServlet。它向 HandlerMapping 询问由哪个处理器处理，拿到处理器和拦截器链后，交给匹配的 HandlerAdapter。 HandlerAdapter 调用参数解析器把请求参数、路径变量、请求体转换成方法参数，执行 Controller 方法；返回值再由返回值处理器处理，@ResponseBody 走 HttpMessageConverter 序列化，否则交给视图解析器渲染。 拦截器的 preHandle 在处理器执行前调用，postHandle 在渲染前调用，afterCompletion 在完成后调用。过滤器属于 Servlet 规范，能拿到原始请求但不能感知处理器；拦截器属于 Spring MVC，能访问处理器信息。",{"id":6515,"title":501,"titles":6516,"content":6517,"level":614},"\u002Fjava-interview\u002Fspring#易错点与自测",[5234],"三级缓存只解决单例的字段\u002Fsetter 循环依赖，不解决构造器注入和原型作用域。 AOP 的一切「注解不生效」几乎都可以归结为「没走代理」。 自动配置不是「无条件装配」，条件注解和加载顺序才是重点。 自测：① 画出 Bean 从实例化到销毁的关键回调顺序；② 说出三种让 @Transactional 恢复生效的改法；③ 解释 REQUIRES_NEW 与 NESTED 在回滚行为上的差异。",{"id":6519,"title":577,"titles":6520,"content":6521,"level":614},"\u002Fjava-interview\u002Fspring#参考资料",[5234],"Spring Framework：IoC 容器与 Bean 生命周期 — 容器、作用域与扩展点 Spring Framework：事务管理 — 传播行为与回滚规则 Spring Boot：自动配置 — 条件注解与加载顺序 Spring Framework：AOP 编程 — 代理机制与自调用问题",{"id":5871,"title":5623,"titles":6523,"content":6524,"level":1206},[],"用一套可复用的排查顺序处理 CPU 飙高、内存上涨、接口变慢、Full GC、线程池与连接池耗尽及发布回滚 排障题考的是「有没有一套稳定的动作」，而不是背工具名。通用顺序是：先确认影响面和最近变更，再用指标缩小范围，然后取证，最后止血与复盘。下面所有命令都应在测试环境先验证，生产执行前评估影响。",{"id":6526,"title":5632,"titles":6527,"content":6528,"level":614},"\u002Fjava-interview\u002Ftroubleshooting#_1-线上-cpu-飙高怎么排查",[5623],"先看整体：top 确认是哪个进程，观察 us、sy、wa 哪个高。用户态高通常是业务计算或 GC；系统态高可能是系统调用、锁竞争或上下文切换。 再定位线程：top -Hp \u003Cpid> 找到最耗 CPU 的线程，把十进制 id 转成十六进制，在 jstack \u003Cpid> 里按 nid=0x... 搜索。连续抓两三次，看它是否停在同一个方法。 业务代码的常见原因：死循环、正则灾难性回溯、超大集合排序、频繁 JSON 序列化、日志拼接、加密计算。如果是 GC 线程占用高，转去看 GC 日志和堆使用。 止血手段包括限流、回滚最近发布、摘除异常实例；不要直接重启了事，先保留线程栈和堆信息，否则根因就丢了。",{"id":6530,"title":5670,"titles":6531,"content":6532,"level":614},"\u002Fjava-interview\u002Ftroubleshooting#_2-内存持续上涨怎么定位",[5623],"先区分堆内和堆外。看 GC 后老年代是否回落：如果每次 Full GC 后仍然上涨，怀疑泄漏；如果 GC 后能回落但很快又涨，可能是流量或缓存策略问题。 堆内取证用 jcmd \u003Cpid> GC.heap_dump 导出，配合 -XX:+HeapDumpOnOutOfMemoryError 自动保存；用 MAT 看支配树、对象数量和 GC Root 路径，重点找「本该释放却被集合或线程持有」的对象。 堆外要看直接内存（NIO、Netty）、元空间（动态生成类）、线程栈和 JIT 代码缓存。jcmd VM.native_memory summary 需要启动时开启 NMT，Netty 可以开泄漏检测。 常见根因：本地缓存无上限、ThreadLocal 未清理、监听器只增不减、批量查询一次加载过多数据。修复前先确认对象的生命周期，不要用「调大堆」掩盖泄漏。",{"id":6534,"title":5700,"titles":6535,"content":6536,"level":614},"\u002Fjava-interview\u002Ftroubleshooting#_3-接口突然变慢排查顺序是什么",[5623],"先看影响范围：是所有接口还是某个接口，是单实例还是全部实例，是否与发布、配置变更、流量高峰同时发生。这一步能排除大半方向。 再看链路：用 traceId 找到耗时最长的那一跳。如果时间花在数据库，看慢查询、锁等待和连接池；花在远程调用，看下游 RT、超时和重试；花在应用内，看线程池队列、锁竞争和 GC 停顿。 注意平均值会骗人，要看 P95、P99 和错误率。重试和超时配置不当会让下游变慢放大成雪崩，表现为「所有服务一起慢」。 还要检查资源：CPU 是否打满、连接数是否耗尽、磁盘是否打满、网络是否有丢包。定位后用限流、扩容、降级或回滚止血，再做根因修复。",{"id":6538,"title":5716,"titles":6539,"content":6540,"level":614},"\u002Fjava-interview\u002Ftroubleshooting#_4-频繁-full-gc-怎么处理",[5623],"先确认频率和原因：用 GC 日志看每次回收前后的占用、停顿时间、晋升量，以及是否伴随 GC overhead limit exceeded 或 promotion failed。 如果是内存泄漏，堆 dump 会显示老年代对象持续增长，必须修代码；如果是堆太小或大对象过多，评估扩容和对象生命周期；如果是元空间不足，检查动态代理、脚本引擎和热部署。 G1 下还要关注 Humongous 对象：超过 Region 一半的大对象会占用连续 Region，频繁创建大数组会触发并发周期甚至 Full GC。 处理顺序是：先止血（扩容、限流、回滚），再取证（GC 日志、堆 dump），最后修复。不要只调 -Xmx，那只是把问题推迟。",{"id":6542,"title":5741,"titles":6543,"content":6544,"level":614},"\u002Fjava-interview\u002Ftroubleshooting#_5-线程池打满有什么表现怎么止损",[5623],"表现通常是接口 RT 上升、超时增多、队列长度持续增长、拒绝数上升，严重时上游开始熔断。异步任务则表现为「提交成功但很久不执行」。 先看监控：活跃线程数、队列长度、完成任务数、拒绝次数、任务耗时分布。再看线程栈，确认线程卡在数据库、远程调用还是锁上。 止血可以临时扩容、提高消费能力、把非核心任务摘出去，或者对入口限流。根因通常是下游变慢、任务无界提交、线程池之间互相阻塞，或者同步调用被放进了异步池。 长期要做池隔离（核心与非核心分开）、有界队列、明确拒绝策略、按下游容量设计并发，并给队列等待时间设上限。",{"id":6546,"title":5757,"titles":6547,"content":6548,"level":614},"\u002Fjava-interview\u002Ftroubleshooting#_6-数据库连接池耗尽怎么办",[5623],"先看连接池指标：活跃连接、空闲连接、等待线程数、获取连接超时次数。活跃连接长期占满且不释放，通常是长事务或连接泄漏。 长事务的常见来源：事务里做远程调用、批量处理过多数据、手动开启事务后异常路径没提交\u002F回滚。连接泄漏则是借出后没有在 finally 中归还。 用数据库侧的证据交叉验证：SHOW PROCESSLIST 看连接状态和执行的 SQL，慢查询日志找长时间运行的语句，锁等待也会让连接一直占用。 止血可以回滚、杀掉异常会话、临时扩容，但根本解决要缩短事务、修复泄漏、优化慢 SQL。连接池不是越大越好，超过数据库承载能力只会让争用更严重。",{"id":6550,"title":5777,"titles":6551,"content":6552,"level":614},"\u002Fjava-interview\u002Ftroubleshooting#_7-redis-或-mq-故障时怎么降级",[5623],"先明确依赖等级：哪些功能可以短时间不可用，哪些必须保底。缓存故障时，可以用本地缓存兜底、限流保护数据库、对非核心读请求返回默认值；同时防止「缓存失效后全部打到数据库」的雪崩。 消息队列故障时，生产端可以降级为同步调用、写入本地消息表稍后补发，或暂时拒绝非核心消息；消费端要保证重复消费幂等，恢复后按积压量平滑追平，避免瞬间打满下游。 降级开关要提前做好并演练，包含触发条件、影响范围、恢复步骤和责任人。不要等故障时才临时改代码。 所有降级都要有监控和告警：降级本身是「有损可用」，必须让团队知道当前处于降级状态。",{"id":6554,"title":5793,"titles":6555,"content":6556,"level":614},"\u002Fjava-interview\u002Ftroubleshooting#_8-发布出问题怎么回滚如何减少这类事故",[5623],"先判断能否快速回滚：应用版本回滚通常最快；数据库变更往往不可逆，所以要做「扩展-收缩」式变更，先加字段、双写、再切读、最后删旧字段，保证新旧版本兼容。 发布策略上，灰度或金丝雀发布先放少量流量，观察错误率、延迟和关键业务指标，再逐步扩大。功能开关可以把「发布」和「上线」分开，出问题时关开关比回滚更快。 回滚前要保留现场：日志、指标、trace、异常堆栈。回滚后确认数据一致性，尤其是消息、缓存和异步任务是否留下中间状态。 事后复盘关注可行动的改进：补充监控、加自动化检查、缩小变更批次、完善回滚脚本。把「这次是谁的错」换成「哪个环节让错误没被更早发现」。",{"id":6558,"title":501,"titles":6559,"content":6560,"level":614},"\u002Fjava-interview\u002Ftroubleshooting#易错点与自测",[5623],"排障先止血再根因，但止血前尽量保留线程栈、堆 dump、GC 日志等现场。 堆内存上涨不等于堆泄漏，直接内存、元空间和线程栈要单独看。 扩连接池、扩线程池常常只是把压力转移到更下游。 自测：① 给出一次 CPU 高的完整命令链；② 说明如何区分内存泄漏和容量不足；③ 设计一个缓存故障时的三级降级方案。",{"id":6562,"title":577,"titles":6563,"content":6564,"level":614},"\u002Fjava-interview\u002Ftroubleshooting#参考资料",[5623],"jcmd 手册（Java 21） — 线程栈、堆信息、堆转储与 JFR Java SE 21 故障排查指南 — OOM、GC 与原生内存问题 Google SRE Book：应急响应 — 事件分级、止血与复盘 HikariCP 配置说明 — 连接池容量与超时参数",1793263558068]