[{"data":1,"prerenderedAt":33410},["ShallowReactive",2],{"java:java-exception-types\u002F":3,"java-wiki-navigation":2158},{"id":4,"title":5,"body":6,"commentId":2145,"description":2146,"difficulty":2147,"draft":2148,"extension":2149,"meta":2150,"navigation":756,"order":2151,"path":2152,"section":2153,"seo":2154,"stem":2155,"updated":2156,"__hash__":2157},"java\u002Fjava\u002Fjava-exception-types.md","Java 的异常都有哪些？",{"type":7,"value":8,"toc":2114},"minimark",[9,13,17,45,64,101,104,113,116,130,133,137,143,153,158,189,195,228,236,289,293,298,368,379,383,388,394,400,403,408,412,415,426,432,469,472,477,552,556,593,596,599,669,672,676,684,687,802,805,914,917,921,924,930,936,942,1016,1020,1025,1046,1051,1081,1084,1090,1094,1099,1162,1165,1220,1223,1271,1275,1281,1374,1383,1418,1429,1442,1446,1449,1522,1525,1599,1605,1608,1642,1646,1649,1653,1716,1719,1723,1791,1794,1804,1808,1814,1894,1897,1901,1905,1939,1942,1946,1973,1976,1980,1983,1987,1990,1994,2068,2072,2110],[10,11,5],"h1",{"id":12},"java-的异常都有哪些",[14,15,16],"h2",{"id":16},"面试回答",[18,19,20,21,25,26,29,30,33,34,36,37,40,41,44],"p",{},"Java 中所有能够被 ",[22,23,24],"code",{},"throw"," 抛出或被 ",[22,27,28],{},"catch"," 捕获的对象都继承自 ",[22,31,32],{},"Throwable","。",[22,35,32],{}," 主要分为 ",[22,38,39],{},"Error"," 和 ",[22,42,43],{},"Exception"," 两个分支。",[18,46,47,49,50,53,54,40,57,60,61,63],{},[22,48,39],{}," 表示 JVM 或运行环境中的严重问题，例如 ",[22,51,52],{},"OutOfMemoryError","、",[22,55,56],{},"StackOverflowError",[22,58,59],{},"NoClassDefFoundError","，应用通常不应该把捕获并继续运行作为常规处理方案。",[22,62,43],{}," 表示应用可以识别和处理的异常情况，其中又分为受检异常和运行时异常。",[18,65,66,67,70,71,73,74,53,77,53,80,83,84,87,88,90,91,53,94,53,97,100],{},"受检异常是除 ",[22,68,69],{},"RuntimeException"," 及其子类之外的 ",[22,72,43],{},"，例如 ",[22,75,76],{},"IOException",[22,78,79],{},"SQLException",[22,81,82],{},"ClassNotFoundException","。编译器要求调用方捕获它们，或者使用 ",[22,85,86],{},"throws"," 继续声明。",[22,89,69],{}," 及其子类属于非受检异常，例如 ",[22,92,93],{},"NullPointerException",[22,95,96],{},"IllegalArgumentException",[22,98,99],{},"IndexOutOfBoundsException","，编译器不强制捕获或声明，通常表示参数、状态或程序逻辑存在问题。",[18,102,103],{},"一句话总结：",[105,106,107],"blockquote",{},[18,108,109],{},[110,111,112],"strong",{},"Throwable 分为 Error 和 Exception；Exception 中除 RuntimeException 体系外通常属于受检异常，而 RuntimeException 与 Error 体系都属于非受检异常。",[14,114,115],{"id":115},"一张图看懂",[18,117,118],{},[119,120,126],"a",{"href":121,"target":122,"rel":123},"\u002Fimages\u002Fwiki\u002Fjava\u002Fjava-exception-types.svg","_blank",[124,125],"noopener","noreferrer",[127,128],"img",{"src":121,"alt":129},"Java Throwable、Error、Exception、RuntimeException 与受检异常的继承关系",[14,131,132],{"id":132},"详细讲解",[14,134,136],{"id":135},"一java-异常体系的根类-throwable","一、Java 异常体系的根类 Throwable",[18,138,139,140,142],{},"Java 的可抛出对象必须是 ",[22,141,32],{}," 或其子类：",[144,145,151],"pre",{"className":146,"code":148,"language":149,"meta":150},[147],"language-text","Object\n  └─ Throwable\n       ├─ Error\n       └─ Exception\n            └─ RuntimeException\n","text","",[22,152,148],{"__ignoreMap":150},[18,154,155,157],{},[22,156,32],{}," 保存了异常处理中常用的信息：",[159,160,161,168,174,180],"ul",{},[162,163,164,167],"li",{},[22,165,166],{},"message","：异常的说明信息；",[162,169,170,173],{},[22,171,172],{},"cause","：导致当前异常的上层原因；",[162,175,176,179],{},[22,177,178],{},"stackTrace","：异常创建时的线程调用栈；",[162,181,182,185,186,33],{},[22,183,184],{},"suppressed","：被抑制的其他异常，常见于 ",[22,187,188],{},"try-with-resources",[18,190,191,192,194],{},"只有 ",[22,193,32],{}," 及其子类实例能够用于：",[144,196,200],{"className":197,"code":198,"language":199,"meta":150,"style":150},"language-java shiki shiki-themes github-light github-dark","throw new IllegalArgumentException(\"订单编号不能为空\");\n","java",[22,201,202],{"__ignoreMap":150},[203,204,207,210,213,217,221,225],"span",{"class":205,"line":206},"line",1,[203,208,24],{"class":209},"szBVR",[203,211,212],{"class":209}," new",[203,214,216],{"class":215},"sScJk"," IllegalArgumentException",[203,218,220],{"class":219},"sVt8B","(",[203,222,224],{"class":223},"sZZnC","\"订单编号不能为空\"",[203,226,227],{"class":219},");\n",[18,229,230,232,233,235],{},[22,231,28],{}," 参数也必须是 ",[22,234,32],{}," 的子类：",[144,237,239],{"className":197,"code":238,"language":199,"meta":150,"style":150},"try {\n    createOrder();\n} catch (IllegalArgumentException exception) {\n    \u002F\u002F 处理异常\n}\n",[22,240,241,249,258,276,283],{"__ignoreMap":150},[203,242,243,246],{"class":205,"line":206},[203,244,245],{"class":209},"try",[203,247,248],{"class":219}," {\n",[203,250,252,255],{"class":205,"line":251},2,[203,253,254],{"class":215},"    createOrder",[203,256,257],{"class":219},"();\n",[203,259,261,264,266,269,273],{"class":205,"line":260},3,[203,262,263],{"class":219},"} ",[203,265,28],{"class":209},[203,267,268],{"class":219}," (IllegalArgumentException ",[203,270,272],{"class":271},"s4XuR","exception",[203,274,275],{"class":219},") {\n",[203,277,279],{"class":205,"line":278},4,[203,280,282],{"class":281},"sJ8bj","    \u002F\u002F 处理异常\n",[203,284,286],{"class":205,"line":285},5,[203,287,288],{"class":219},"}\n",[14,290,292],{"id":291},"二error-表示什么","二、Error 表示什么",[18,294,295,297],{},[22,296,39],{}," 表示 JVM、类加载或运行环境出现的严重问题。常见类型包括：",[299,300,301,314],"table",{},[302,303,304],"thead",{},[305,306,307,311],"tr",{},[308,309,310],"th",{},"类型",[308,312,313],{},"通常表示",[315,316,317,327,336,345,355],"tbody",{},[305,318,319,324],{},[320,321,322],"td",{},[22,323,52],{},[320,325,326],{},"Java 堆、元空间、直接内存或本地线程等资源无法继续分配",[305,328,329,333],{},[320,330,331],{},[22,332,56],{},[320,334,335],{},"线程栈因递归过深等原因溢出",[305,337,338,342],{},[320,339,340],{},[22,341,59],{},[320,343,344],{},"运行时无法使用原本期望存在的类定义",[305,346,347,352],{},[320,348,349],{},[22,350,351],{},"ExceptionInInitializerError",[320,353,354],{},"类初始化方法执行失败",[305,356,357,362],{},[320,358,359],{},[22,360,361],{},"AssertionError",[320,363,364,367],{},[22,365,366],{},"assert"," 断言失败",[18,369,370,372,373,375,376,378],{},[22,371,39],{}," 也能被 ",[22,374,28],{},"，但“语法上能够捕获”不代表“业务上应该捕获后继续运行”。例如发生 ",[22,377,52],{}," 后，进程可能已经没有足够资源完成复杂的恢复逻辑。常见做法是保留必要诊断信息，由进程管理或容器完成重启，并从根因上处理资源耗尽问题。",[14,380,382],{"id":381},"三exception-表示什么","三、Exception 表示什么",[18,384,385,387],{},[22,386,43],{}," 表示程序执行期间能够被应用识别的异常情况。它包含两类：",[144,389,392],{"className":390,"code":391,"language":149,"meta":150},[147],"Exception\n  ├─ 受检异常\n  │    ├─ IOException\n  │    ├─ SQLException\n  │    ├─ ClassNotFoundException\n  │    └─ InterruptedException\n  │\n  └─ RuntimeException\n       ├─ NullPointerException\n       ├─ IllegalArgumentException\n       ├─ IllegalStateException\n       └─ IndexOutOfBoundsException\n",[22,393,391],{"__ignoreMap":150},[18,395,396,397,399],{},"这里的分类依据不是异常是否真的发生在“运行时”。所有异常最终都发生在程序运行过程中，",[22,398,69],{}," 只是一个具体的继承分支。",[18,401,402],{},"真正的区别是：",[105,404,405],{},[18,406,407],{},"编译器是否强制调用方捕获或声明这个异常。",[14,409,411],{"id":410},"四什么是受检异常","四、什么是受检异常",[18,413,414],{},"受检异常也称 Checked Exception。对于常见业务代码，可以理解为：",[105,416,417],{},[18,418,419,420,422,423,425],{},"继承自 ",[22,421,43],{},"，但不继承自 ",[22,424,69],{}," 的异常。",[18,427,428,429,431],{},"例如读取文件可能抛出 ",[22,430,76],{},"：",[144,433,435],{"className":197,"code":434,"language":199,"meta":150,"style":150},"public String readConfig(Path path) throws IOException {\n    return Files.readString(path, StandardCharsets.UTF_8);\n}\n",[22,436,437,451,465],{"__ignoreMap":150},[203,438,439,442,445,448],{"class":205,"line":206},[203,440,441],{"class":209},"public",[203,443,444],{"class":219}," String ",[203,446,447],{"class":215},"readConfig",[203,449,450],{"class":219},"(Path path) throws IOException {\n",[203,452,453,456,459,462],{"class":205,"line":251},[203,454,455],{"class":209},"    return",[203,457,458],{"class":219}," Files.",[203,460,461],{"class":215},"readString",[203,463,464],{"class":219},"(path, StandardCharsets.UTF_8);\n",[203,466,467],{"class":205,"line":260},[203,468,288],{"class":219},[18,470,471],{},"调用方必须选择一种方式。",[473,474,476],"h3",{"id":475},"_1-捕获并处理","1. 捕获并处理",[144,478,480],{"className":197,"code":479,"language":199,"meta":150,"style":150},"try {\n    String config = readConfig(path);\n    applyConfig(config);\n} catch (IOException exception) {\n    log.error(\"读取配置文件失败\", exception);\n    useDefaultConfig();\n}\n",[22,481,482,488,502,510,523,539,547],{"__ignoreMap":150},[203,483,484,486],{"class":205,"line":206},[203,485,245],{"class":209},[203,487,248],{"class":219},[203,489,490,493,496,499],{"class":205,"line":251},[203,491,492],{"class":219},"    String config ",[203,494,495],{"class":209},"=",[203,497,498],{"class":215}," readConfig",[203,500,501],{"class":219},"(path);\n",[203,503,504,507],{"class":205,"line":260},[203,505,506],{"class":215},"    applyConfig",[203,508,509],{"class":219},"(config);\n",[203,511,512,514,516,519,521],{"class":205,"line":278},[203,513,263],{"class":219},[203,515,28],{"class":209},[203,517,518],{"class":219}," (IOException ",[203,520,272],{"class":271},[203,522,275],{"class":219},[203,524,525,528,531,533,536],{"class":205,"line":285},[203,526,527],{"class":219},"    log.",[203,529,530],{"class":215},"error",[203,532,220],{"class":219},[203,534,535],{"class":223},"\"读取配置文件失败\"",[203,537,538],{"class":219},", exception);\n",[203,540,542,545],{"class":205,"line":541},6,[203,543,544],{"class":215},"    useDefaultConfig",[203,546,257],{"class":219},[203,548,550],{"class":205,"line":549},7,[203,551,288],{"class":219},[473,553,555],{"id":554},"_2-使用-throws-继续声明","2. 使用 throws 继续声明",[144,557,559],{"className":197,"code":558,"language":199,"meta":150,"style":150},"public void initialize(Path path) throws IOException {\n    String config = readConfig(path);\n    applyConfig(config);\n}\n",[22,560,561,573,583,589],{"__ignoreMap":150},[203,562,563,565,568,571],{"class":205,"line":206},[203,564,441],{"class":209},[203,566,567],{"class":209}," void",[203,569,570],{"class":215}," initialize",[203,572,450],{"class":219},[203,574,575,577,579,581],{"class":205,"line":251},[203,576,492],{"class":219},[203,578,495],{"class":209},[203,580,498],{"class":215},[203,582,501],{"class":219},[203,584,585,587],{"class":205,"line":260},[203,586,506],{"class":215},[203,588,509],{"class":219},[203,590,591],{"class":205,"line":278},[203,592,288],{"class":219},[18,594,595],{},"如果既不捕获也不声明，代码无法通过编译。",[18,597,598],{},"常见受检异常包括：",[299,600,601,610],{},[302,602,603],{},[305,604,605,607],{},[308,606,310],{},[308,608,609],{},"常见场景",[315,611,612,621,630,639,649,659],{},[305,613,614,618],{},[320,615,616],{},[22,617,76],{},[320,619,620],{},"文件或网络 I\u002FO 失败",[305,622,623,627],{},[320,624,625],{},[22,626,79],{},[320,628,629],{},"通过 JDBC 直接访问数据库失败",[305,631,632,636],{},[320,633,634],{},[22,635,82],{},[320,637,638],{},"按类名动态加载类时找不到目标类",[305,640,641,646],{},[320,642,643],{},[22,644,645],{},"InterruptedException",[320,647,648],{},"阻塞线程收到中断请求",[305,650,651,656],{},[320,652,653],{},[22,654,655],{},"ParseException",[320,657,658],{},"文本无法按约定格式解析",[305,660,661,666],{},[320,662,663],{},[22,664,665],{},"ReflectiveOperationException",[320,667,668],{},"反射查找或调用失败的公共父类",[18,670,671],{},"受检异常通常用于表示调用方有机会处理或恢复的外部失败，但这是一种设计选择，并不意味着所有可恢复问题都必须设计成受检异常。",[14,673,675],{"id":674},"五什么是-runtimeexception","五、什么是 RuntimeException",[18,677,678,680,681,683],{},[22,679,69],{}," 及其子类属于非受检异常，也称 Unchecked Exception。编译器不强制调用方捕获或在 ",[22,682,86],{}," 中声明。",[18,685,686],{},"例如：",[144,688,690],{"className":197,"code":689,"language":199,"meta":150,"style":150},"public Order findOrder(String orderId) {\n    if (orderId == null || orderId.isBlank()) {\n        throw new IllegalArgumentException(\"订单编号不能为空\");\n    }\n\n    return repository.findById(orderId)\n            .orElseThrow(() ->\n                    new OrderNotFoundException(orderId));\n}\n",[22,691,692,705,732,747,752,758,771,785,797],{"__ignoreMap":150},[203,693,694,696,699,702],{"class":205,"line":206},[203,695,441],{"class":209},[203,697,698],{"class":219}," Order ",[203,700,701],{"class":215},"findOrder",[203,703,704],{"class":219},"(String orderId) {\n",[203,706,707,710,713,716,720,723,726,729],{"class":205,"line":251},[203,708,709],{"class":209},"    if",[203,711,712],{"class":219}," (orderId ",[203,714,715],{"class":209},"==",[203,717,719],{"class":718},"sj4cs"," null",[203,721,722],{"class":209}," ||",[203,724,725],{"class":219}," orderId.",[203,727,728],{"class":215},"isBlank",[203,730,731],{"class":219},"()) {\n",[203,733,734,737,739,741,743,745],{"class":205,"line":260},[203,735,736],{"class":209},"        throw",[203,738,212],{"class":209},[203,740,216],{"class":215},[203,742,220],{"class":219},[203,744,224],{"class":223},[203,746,227],{"class":219},[203,748,749],{"class":205,"line":278},[203,750,751],{"class":219},"    }\n",[203,753,754],{"class":205,"line":285},[203,755,757],{"emptyLinePlaceholder":756},true,"\n",[203,759,760,762,765,768],{"class":205,"line":541},[203,761,455],{"class":209},[203,763,764],{"class":219}," repository.",[203,766,767],{"class":215},"findById",[203,769,770],{"class":219},"(orderId)\n",[203,772,773,776,779,782],{"class":205,"line":549},[203,774,775],{"class":219},"            .",[203,777,778],{"class":215},"orElseThrow",[203,780,781],{"class":219},"(() ",[203,783,784],{"class":209},"->\n",[203,786,788,791,794],{"class":205,"line":787},8,[203,789,790],{"class":209},"                    new",[203,792,793],{"class":215}," OrderNotFoundException",[203,795,796],{"class":219},"(orderId));\n",[203,798,800],{"class":205,"line":799},9,[203,801,288],{"class":219},[18,803,804],{},"常见运行时异常包括：",[299,806,807,815],{},[302,808,809],{},[305,810,811,813],{},[308,812,310],{},[308,814,313],{},[315,816,817,826,835,845,854,864,874,884,894,904],{},[305,818,819,823],{},[320,820,821],{},[22,822,93],{},[320,824,825],{},"使用了空引用",[305,827,828,832],{},[320,829,830],{},[22,831,96],{},[320,833,834],{},"方法参数不符合要求",[305,836,837,842],{},[320,838,839],{},[22,840,841],{},"IllegalStateException",[320,843,844],{},"对象当前状态不允许执行该操作",[305,846,847,851],{},[320,848,849],{},[22,850,99],{},[320,852,853],{},"数组、字符串或集合下标越界",[305,855,856,861],{},[320,857,858],{},[22,859,860],{},"ClassCastException",[320,862,863],{},"对象无法转换为目标类型",[305,865,866,871],{},[320,867,868],{},[22,869,870],{},"ArithmeticException",[320,872,873],{},"整数除零等算术错误",[305,875,876,881],{},[320,877,878],{},[22,879,880],{},"ConcurrentModificationException",[320,882,883],{},"遍历期间检测到不允许的结构性修改",[305,885,886,891],{},[320,887,888],{},[22,889,890],{},"UnsupportedOperationException",[320,892,893],{},"当前实现不支持所调用的操作",[305,895,896,901],{},[320,897,898],{},[22,899,900],{},"NoSuchElementException",[320,902,903],{},"请求的元素不存在",[305,905,906,911],{},[320,907,908],{},[22,909,910],{},"RejectedExecutionException",[320,912,913],{},"线程池无法接受新任务",[18,915,916],{},"非受检异常不是“不需要处理”，而是编译器不强制处理。应用仍然可以在合适的边界进行统一转换、记录或返回错误响应。",[14,918,920],{"id":919},"六受检异常与非受检异常的准确边界","六、受检异常与非受检异常的准确边界",[18,922,923],{},"从 Java 编译期检查规则看：",[144,925,928],{"className":926,"code":927,"language":149,"meta":150},[147],"受检异常\n  = Throwable 的子类\n  - RuntimeException 体系\n  - Error 体系\n\n非受检异常\n  = RuntimeException 及其子类\n  + Error 及其子类\n",[22,929,927],{"__ignoreMap":150},[18,931,932,933,935],{},"日常开发中通常不会直接继承 ",[22,934,32],{},"，因此经常简化成：",[144,937,940],{"className":938,"code":939,"language":149,"meta":150},[147],"Exception 中除 RuntimeException 体系外的是受检异常\nRuntimeException 和 Error 体系是非受检异常\n",[22,941,939],{"__ignoreMap":150},[299,943,944,957],{},[302,945,946],{},[305,947,948,951,954],{},[308,949,950],{},"对比项",[308,952,953],{},"受检异常",[308,955,956],{},"非受检异常",[315,958,959,974,985,995,1005],{},[305,960,961,964,968],{},[320,962,963],{},"典型父类",[320,965,966],{},[22,967,43],{},[320,969,970,53,972],{},[22,971,69],{},[22,973,39],{},[305,975,976,979,982],{},[320,977,978],{},"编译器强制处理",[320,980,981],{},"是",[320,983,984],{},"否",[305,986,987,990,993],{},[320,988,989],{},"是否可以捕获",[320,991,992],{},"可以",[320,994,992],{},[305,996,997,1000,1002],{},[320,998,999],{},"是否可以声明 throws",[320,1001,992],{},[320,1003,1004],{},"可以，但不强制",[305,1006,1007,1010,1013],{},[320,1008,1009],{},"常见含义",[320,1011,1012],{},"调用方可能处理的外部失败",[320,1014,1015],{},"程序错误、非法状态或严重运行环境问题",[14,1017,1019],{"id":1018},"七throw-和-throws-有什么区别","七、throw 和 throws 有什么区别",[18,1021,1022,1024],{},[22,1023,24],{}," 用于真正抛出一个异常对象：",[144,1026,1028],{"className":197,"code":1027,"language":199,"meta":150,"style":150},"throw new IllegalStateException(\"订单已经关闭\");\n",[22,1029,1030],{"__ignoreMap":150},[203,1031,1032,1034,1036,1039,1041,1044],{"class":205,"line":206},[203,1033,24],{"class":209},[203,1035,212],{"class":209},[203,1037,1038],{"class":215}," IllegalStateException",[203,1040,220],{"class":219},[203,1042,1043],{"class":223},"\"订单已经关闭\"",[203,1045,227],{"class":219},[18,1047,1048,1050],{},[22,1049,86],{}," 写在方法声明中，表示该方法可能把异常继续交给调用方：",[144,1052,1054],{"className":197,"code":1053,"language":199,"meta":150,"style":150},"public String read(Path path) throws IOException {\n    return Files.readString(path);\n}\n",[22,1055,1056,1067,1077],{"__ignoreMap":150},[203,1057,1058,1060,1062,1065],{"class":205,"line":206},[203,1059,441],{"class":209},[203,1061,444],{"class":219},[203,1063,1064],{"class":215},"read",[203,1066,450],{"class":219},[203,1068,1069,1071,1073,1075],{"class":205,"line":251},[203,1070,455],{"class":209},[203,1072,458],{"class":219},[203,1074,461],{"class":215},[203,1076,501],{"class":219},[203,1078,1079],{"class":205,"line":260},[203,1080,288],{"class":219},[18,1082,1083],{},"可以记成：",[144,1085,1088],{"className":1086,"code":1087,"language":149,"meta":150},[147],"throw  → 抛出一个具体异常对象\nthrows → 声明方法可能向外传播哪些异常\n",[22,1089,1087],{"__ignoreMap":150},[14,1091,1093],{"id":1092},"八异常是如何匹配-catch-的","八、异常是如何匹配 catch 的",[18,1095,1096,1097,431],{},"异常抛出后，JVM 会沿调用栈向上寻找第一个类型匹配的 ",[22,1098,28],{},[144,1100,1102],{"className":197,"code":1101,"language":199,"meta":150,"style":150},"try {\n    process(input);\n} catch (IllegalArgumentException exception) {\n    handleInvalidArgument(exception);\n} catch (RuntimeException exception) {\n    handleRuntimeFailure(exception);\n}\n",[22,1103,1104,1110,1118,1130,1138,1151,1158],{"__ignoreMap":150},[203,1105,1106,1108],{"class":205,"line":206},[203,1107,245],{"class":209},[203,1109,248],{"class":219},[203,1111,1112,1115],{"class":205,"line":251},[203,1113,1114],{"class":215},"    process",[203,1116,1117],{"class":219},"(input);\n",[203,1119,1120,1122,1124,1126,1128],{"class":205,"line":260},[203,1121,263],{"class":219},[203,1123,28],{"class":209},[203,1125,268],{"class":219},[203,1127,272],{"class":271},[203,1129,275],{"class":219},[203,1131,1132,1135],{"class":205,"line":278},[203,1133,1134],{"class":215},"    handleInvalidArgument",[203,1136,1137],{"class":219},"(exception);\n",[203,1139,1140,1142,1144,1147,1149],{"class":205,"line":285},[203,1141,263],{"class":219},[203,1143,28],{"class":209},[203,1145,1146],{"class":219}," (RuntimeException ",[203,1148,272],{"class":271},[203,1150,275],{"class":219},[203,1152,1153,1156],{"class":205,"line":541},[203,1154,1155],{"class":215},"    handleRuntimeFailure",[203,1157,1137],{"class":219},[203,1159,1160],{"class":205,"line":549},[203,1161,288],{"class":219},[18,1163,1164],{},"子类异常必须写在父类异常前面。如果先捕获父类，后面的子类分支永远无法到达，编译器会直接报错：",[144,1166,1168],{"className":197,"code":1167,"language":199,"meta":150,"style":150},"try {\n    process(input);\n} catch (RuntimeException exception) {\n    \u002F\u002F 已经包含 IllegalArgumentException\n} catch (IllegalArgumentException exception) {\n    \u002F\u002F 无法到达，不能通过编译\n}\n",[22,1169,1170,1176,1182,1194,1199,1211,1216],{"__ignoreMap":150},[203,1171,1172,1174],{"class":205,"line":206},[203,1173,245],{"class":209},[203,1175,248],{"class":219},[203,1177,1178,1180],{"class":205,"line":251},[203,1179,1114],{"class":215},[203,1181,1117],{"class":219},[203,1183,1184,1186,1188,1190,1192],{"class":205,"line":260},[203,1185,263],{"class":219},[203,1187,28],{"class":209},[203,1189,1146],{"class":219},[203,1191,272],{"class":271},[203,1193,275],{"class":219},[203,1195,1196],{"class":205,"line":278},[203,1197,1198],{"class":281},"    \u002F\u002F 已经包含 IllegalArgumentException\n",[203,1200,1201,1203,1205,1207,1209],{"class":205,"line":285},[203,1202,263],{"class":219},[203,1204,28],{"class":209},[203,1206,268],{"class":219},[203,1208,272],{"class":271},[203,1210,275],{"class":219},[203,1212,1213],{"class":205,"line":541},[203,1214,1215],{"class":281},"    \u002F\u002F 无法到达，不能通过编译\n",[203,1217,1218],{"class":205,"line":549},[203,1219,288],{"class":219},[18,1221,1222],{},"多个互不具有继承关系的异常可以使用 multi-catch：",[144,1224,1226],{"className":197,"code":1225,"language":199,"meta":150,"style":150},"try {\n    loadData();\n} catch (IOException | SQLException exception) {\n    log.error(\"加载数据失败\", exception);\n}\n",[22,1227,1228,1234,1241,1254,1267],{"__ignoreMap":150},[203,1229,1230,1232],{"class":205,"line":206},[203,1231,245],{"class":209},[203,1233,248],{"class":219},[203,1235,1236,1239],{"class":205,"line":251},[203,1237,1238],{"class":215},"    loadData",[203,1240,257],{"class":219},[203,1242,1243,1245,1247,1250,1252],{"class":205,"line":260},[203,1244,263],{"class":219},[203,1246,28],{"class":209},[203,1248,1249],{"class":219}," (IOException | SQLException ",[203,1251,272],{"class":271},[203,1253,275],{"class":219},[203,1255,1256,1258,1260,1262,1265],{"class":205,"line":278},[203,1257,527],{"class":219},[203,1259,530],{"class":215},[203,1261,220],{"class":219},[203,1263,1264],{"class":223},"\"加载数据失败\"",[203,1266,538],{"class":219},[203,1268,1269],{"class":205,"line":285},[203,1270,288],{"class":219},[14,1272,1274],{"id":1273},"九finally-和-try-with-resources","九、finally 和 try-with-resources",[18,1276,1277,1280],{},[22,1278,1279],{},"finally"," 通常用于执行清理逻辑：",[144,1282,1284],{"className":197,"code":1283,"language":199,"meta":150,"style":150},"InputStream input = null;\n\ntry {\n    input = Files.newInputStream(path);\n    return input.read();\n} finally {\n    if (input != null) {\n        input.close();\n    }\n}\n",[22,1285,1286,1298,1302,1308,1322,1333,1341,1355,1365,1369],{"__ignoreMap":150},[203,1287,1288,1291,1293,1295],{"class":205,"line":206},[203,1289,1290],{"class":219},"InputStream input ",[203,1292,495],{"class":209},[203,1294,719],{"class":718},[203,1296,1297],{"class":219},";\n",[203,1299,1300],{"class":205,"line":251},[203,1301,757],{"emptyLinePlaceholder":756},[203,1303,1304,1306],{"class":205,"line":260},[203,1305,245],{"class":209},[203,1307,248],{"class":219},[203,1309,1310,1313,1315,1317,1320],{"class":205,"line":278},[203,1311,1312],{"class":219},"    input ",[203,1314,495],{"class":209},[203,1316,458],{"class":219},[203,1318,1319],{"class":215},"newInputStream",[203,1321,501],{"class":219},[203,1323,1324,1326,1329,1331],{"class":205,"line":285},[203,1325,455],{"class":209},[203,1327,1328],{"class":219}," input.",[203,1330,1064],{"class":215},[203,1332,257],{"class":219},[203,1334,1335,1337,1339],{"class":205,"line":541},[203,1336,263],{"class":219},[203,1338,1279],{"class":209},[203,1340,248],{"class":219},[203,1342,1343,1345,1348,1351,1353],{"class":205,"line":549},[203,1344,709],{"class":209},[203,1346,1347],{"class":219}," (input ",[203,1349,1350],{"class":209},"!=",[203,1352,719],{"class":718},[203,1354,275],{"class":219},[203,1356,1357,1360,1363],{"class":205,"line":787},[203,1358,1359],{"class":219},"        input.",[203,1361,1362],{"class":215},"close",[203,1364,257],{"class":219},[203,1366,1367],{"class":205,"line":799},[203,1368,751],{"class":219},[203,1370,1372],{"class":205,"line":1371},10,[203,1373,288],{"class":219},[18,1375,1376,1377,1380,1381,431],{},"对于实现了 ",[22,1378,1379],{},"AutoCloseable"," 的资源，更推荐使用 ",[22,1382,188],{},[144,1384,1386],{"className":197,"code":1385,"language":199,"meta":150,"style":150},"try (InputStream input = Files.newInputStream(path)) {\n    return input.read();\n}\n",[22,1387,1388,1404,1414],{"__ignoreMap":150},[203,1389,1390,1392,1395,1397,1399,1401],{"class":205,"line":206},[203,1391,245],{"class":209},[203,1393,1394],{"class":219}," (InputStream input ",[203,1396,495],{"class":209},[203,1398,458],{"class":219},[203,1400,1319],{"class":215},[203,1402,1403],{"class":219},"(path)) {\n",[203,1405,1406,1408,1410,1412],{"class":205,"line":251},[203,1407,455],{"class":209},[203,1409,1328],{"class":219},[203,1411,1064],{"class":215},[203,1413,257],{"class":219},[203,1415,1416],{"class":205,"line":260},[203,1417,288],{"class":219},[18,1419,1420,1421,1424,1425,1428],{},"它会自动关闭资源。如果业务代码和 ",[22,1422,1423],{},"close()"," 同时抛出异常，业务异常会作为主要异常继续传播，关闭资源产生的异常会记录在 ",[22,1426,1427],{},"getSuppressed()"," 中，避免清理异常覆盖真正的失败原因。",[18,1430,1431,1432,1434,1435,1438,1439,1441],{},"不要在 ",[22,1433,1279],{}," 中 ",[22,1436,1437],{},"return","，因为它可能覆盖 ",[22,1440,245],{}," 中的返回结果或正在传播的异常。",[14,1443,1445],{"id":1444},"十异常链与异常转换","十、异常链与异常转换",[18,1447,1448],{},"应用分层时，可以把底层异常转换成更符合当前业务语义的异常，但应该保留原始原因：",[144,1450,1452],{"className":197,"code":1451,"language":199,"meta":150,"style":150},"try {\n    return repository.findById(orderId);\n} catch (SQLException exception) {\n    throw new OrderQueryException(\n            \"查询订单失败：\" + orderId,\n            exception\n    );\n}\n",[22,1453,1454,1460,1471,1484,1497,1508,1513,1518],{"__ignoreMap":150},[203,1455,1456,1458],{"class":205,"line":206},[203,1457,245],{"class":209},[203,1459,248],{"class":219},[203,1461,1462,1464,1466,1468],{"class":205,"line":251},[203,1463,455],{"class":209},[203,1465,764],{"class":219},[203,1467,767],{"class":215},[203,1469,1470],{"class":219},"(orderId);\n",[203,1472,1473,1475,1477,1480,1482],{"class":205,"line":260},[203,1474,263],{"class":219},[203,1476,28],{"class":209},[203,1478,1479],{"class":219}," (SQLException ",[203,1481,272],{"class":271},[203,1483,275],{"class":219},[203,1485,1486,1489,1491,1494],{"class":205,"line":278},[203,1487,1488],{"class":209},"    throw",[203,1490,212],{"class":209},[203,1492,1493],{"class":215}," OrderQueryException",[203,1495,1496],{"class":219},"(\n",[203,1498,1499,1502,1505],{"class":205,"line":285},[203,1500,1501],{"class":223},"            \"查询订单失败：\"",[203,1503,1504],{"class":209}," +",[203,1506,1507],{"class":219}," orderId,\n",[203,1509,1510],{"class":205,"line":541},[203,1511,1512],{"class":219},"            exception\n",[203,1514,1515],{"class":205,"line":549},[203,1516,1517],{"class":219},"    );\n",[203,1519,1520],{"class":205,"line":787},[203,1521,288],{"class":219},[18,1523,1524],{},"对应的自定义异常：",[144,1526,1528],{"className":197,"code":1527,"language":199,"meta":150,"style":150},"public class OrderQueryException extends RuntimeException {\n\n    public OrderQueryException(\n            String message,\n            Throwable cause\n    ) {\n        super(message, cause);\n    }\n}\n",[22,1529,1530,1547,1551,1560,1570,1578,1583,1591,1595],{"__ignoreMap":150},[203,1531,1532,1534,1537,1539,1542,1545],{"class":205,"line":206},[203,1533,441],{"class":209},[203,1535,1536],{"class":209}," class",[203,1538,1493],{"class":215},[203,1540,1541],{"class":209}," extends",[203,1543,1544],{"class":215}," RuntimeException",[203,1546,248],{"class":219},[203,1548,1549],{"class":205,"line":251},[203,1550,757],{"emptyLinePlaceholder":756},[203,1552,1553,1556,1558],{"class":205,"line":260},[203,1554,1555],{"class":209},"    public",[203,1557,1493],{"class":215},[203,1559,1496],{"class":219},[203,1561,1562,1565,1567],{"class":205,"line":278},[203,1563,1564],{"class":219},"            String ",[203,1566,166],{"class":271},[203,1568,1569],{"class":219},",\n",[203,1571,1572,1575],{"class":205,"line":285},[203,1573,1574],{"class":219},"            Throwable ",[203,1576,1577],{"class":271},"cause\n",[203,1579,1580],{"class":205,"line":541},[203,1581,1582],{"class":219},"    ) {\n",[203,1584,1585,1588],{"class":205,"line":549},[203,1586,1587],{"class":718},"        super",[203,1589,1590],{"class":219},"(message, cause);\n",[203,1592,1593],{"class":205,"line":787},[203,1594,751],{"class":219},[203,1596,1597],{"class":205,"line":799},[203,1598,288],{"class":219},[18,1600,1601,1602,1604],{},"这样日志既能表达“查询订单失败”的业务语义，也能通过 ",[22,1603,172],{}," 保留原始数据库异常和调用栈。",[18,1606,1607],{},"错误写法是只保留新消息，丢失原始异常：",[144,1609,1611],{"className":197,"code":1610,"language":199,"meta":150,"style":150},"catch (SQLException exception) {\n    throw new OrderQueryException(\"查询订单失败\");\n}\n",[22,1612,1613,1623,1638],{"__ignoreMap":150},[203,1614,1615,1617,1619,1621],{"class":205,"line":206},[203,1616,28],{"class":209},[203,1618,1479],{"class":219},[203,1620,272],{"class":271},[203,1622,275],{"class":219},[203,1624,1625,1627,1629,1631,1633,1636],{"class":205,"line":251},[203,1626,1488],{"class":209},[203,1628,212],{"class":209},[203,1630,1493],{"class":215},[203,1632,220],{"class":219},[203,1634,1635],{"class":223},"\"查询订单失败\"",[203,1637,227],{"class":219},[203,1639,1640],{"class":205,"line":260},[203,1641,288],{"class":219},[14,1643,1645],{"id":1644},"十一自定义异常应该继承谁","十一、自定义异常应该继承谁",[18,1647,1648],{},"选择自定义异常父类时，可以先判断调用方是否被要求立即处理：",[473,1650,1652],{"id":1651},"_1-继承-runtimeexception","1. 继承 RuntimeException",[144,1654,1656],{"className":197,"code":1655,"language":199,"meta":150,"style":150},"public class OrderNotFoundException\n        extends RuntimeException {\n\n    public OrderNotFoundException(String orderId) {\n        super(\"订单不存在：\" + orderId);\n    }\n}\n",[22,1657,1658,1667,1676,1680,1694,1708,1712],{"__ignoreMap":150},[203,1659,1660,1662,1664],{"class":205,"line":206},[203,1661,441],{"class":209},[203,1663,1536],{"class":209},[203,1665,1666],{"class":215}," OrderNotFoundException\n",[203,1668,1669,1672,1674],{"class":205,"line":251},[203,1670,1671],{"class":209},"        extends",[203,1673,1544],{"class":215},[203,1675,248],{"class":219},[203,1677,1678],{"class":205,"line":260},[203,1679,757],{"emptyLinePlaceholder":756},[203,1681,1682,1684,1686,1689,1692],{"class":205,"line":278},[203,1683,1555],{"class":209},[203,1685,793],{"class":215},[203,1687,1688],{"class":219},"(String ",[203,1690,1691],{"class":271},"orderId",[203,1693,275],{"class":219},[203,1695,1696,1698,1700,1703,1705],{"class":205,"line":285},[203,1697,1587],{"class":718},[203,1699,220],{"class":219},[203,1701,1702],{"class":223},"\"订单不存在：\"",[203,1704,1504],{"class":209},[203,1706,1707],{"class":219}," orderId);\n",[203,1709,1710],{"class":205,"line":541},[203,1711,751],{"class":219},[203,1713,1714],{"class":205,"line":549},[203,1715,288],{"class":219},[18,1717,1718],{},"适合业务校验失败、资源不存在、状态冲突等通常由统一异常处理器转换为错误响应的场景。",[473,1720,1722],{"id":1721},"_2-继承-exception","2. 继承 Exception",[144,1724,1726],{"className":197,"code":1725,"language":199,"meta":150,"style":150},"public class ReportExportException\n        extends Exception {\n\n    public ReportExportException(\n            String message,\n            Throwable cause\n    ) {\n        super(message, cause);\n    }\n}\n",[22,1727,1728,1737,1746,1750,1759,1767,1773,1777,1783,1787],{"__ignoreMap":150},[203,1729,1730,1732,1734],{"class":205,"line":206},[203,1731,441],{"class":209},[203,1733,1536],{"class":209},[203,1735,1736],{"class":215}," ReportExportException\n",[203,1738,1739,1741,1744],{"class":205,"line":251},[203,1740,1671],{"class":209},[203,1742,1743],{"class":215}," Exception",[203,1745,248],{"class":219},[203,1747,1748],{"class":205,"line":260},[203,1749,757],{"emptyLinePlaceholder":756},[203,1751,1752,1754,1757],{"class":205,"line":278},[203,1753,1555],{"class":209},[203,1755,1756],{"class":215}," ReportExportException",[203,1758,1496],{"class":219},[203,1760,1761,1763,1765],{"class":205,"line":285},[203,1762,1564],{"class":219},[203,1764,166],{"class":271},[203,1766,1569],{"class":219},[203,1768,1769,1771],{"class":205,"line":541},[203,1770,1574],{"class":219},[203,1772,1577],{"class":271},[203,1774,1775],{"class":205,"line":549},[203,1776,1582],{"class":219},[203,1778,1779,1781],{"class":205,"line":787},[203,1780,1587],{"class":718},[203,1782,1590],{"class":219},[203,1784,1785],{"class":205,"line":799},[203,1786,751],{"class":219},[203,1788,1789],{"class":205,"line":1371},[203,1790,288],{"class":219},[18,1792,1793],{},"适合调用方确实需要显式决定重试、降级、改用备用方案或终止流程的场景。",[18,1795,1796,1797,1800,1801,1803],{},"不要只是为了避免编写 ",[22,1798,1799],{},"try-catch","，就把所有异常都设计成 ",[22,1802,69],{},"；也不要把每个业务失败都设计成受检异常，否则会产生大量只负责继续抛出的模板代码。",[14,1805,1807],{"id":1806},"十二interruptedexception-要特别处理","十二、InterruptedException 要特别处理",[18,1809,1810,1811,1813],{},"捕获 ",[22,1812,645],{}," 后，如果当前方法不能直接继续抛出，通常应该恢复线程的中断标记：",[144,1815,1817],{"className":197,"code":1816,"language":199,"meta":150,"style":150},"try {\n    blockingQueue.take();\n} catch (InterruptedException exception) {\n    Thread.currentThread().interrupt();\n    throw new TaskInterruptedException(\n            \"任务等待被中断\",\n            exception\n    );\n}\n",[22,1818,1819,1825,1835,1848,1864,1875,1882,1886,1890],{"__ignoreMap":150},[203,1820,1821,1823],{"class":205,"line":206},[203,1822,245],{"class":209},[203,1824,248],{"class":219},[203,1826,1827,1830,1833],{"class":205,"line":251},[203,1828,1829],{"class":219},"    blockingQueue.",[203,1831,1832],{"class":215},"take",[203,1834,257],{"class":219},[203,1836,1837,1839,1841,1844,1846],{"class":205,"line":260},[203,1838,263],{"class":219},[203,1840,28],{"class":209},[203,1842,1843],{"class":219}," (InterruptedException ",[203,1845,272],{"class":271},[203,1847,275],{"class":219},[203,1849,1850,1853,1856,1859,1862],{"class":205,"line":278},[203,1851,1852],{"class":219},"    Thread.",[203,1854,1855],{"class":215},"currentThread",[203,1857,1858],{"class":219},"().",[203,1860,1861],{"class":215},"interrupt",[203,1863,257],{"class":219},[203,1865,1866,1868,1870,1873],{"class":205,"line":285},[203,1867,1488],{"class":209},[203,1869,212],{"class":209},[203,1871,1872],{"class":215}," TaskInterruptedException",[203,1874,1496],{"class":219},[203,1876,1877,1880],{"class":205,"line":541},[203,1878,1879],{"class":223},"            \"任务等待被中断\"",[203,1881,1569],{"class":219},[203,1883,1884],{"class":205,"line":549},[203,1885,1512],{"class":219},[203,1887,1888],{"class":205,"line":787},[203,1889,1517],{"class":219},[203,1891,1892],{"class":205,"line":799},[203,1893,288],{"class":219},[18,1895,1896],{},"如果只记录日志然后继续执行，上层代码可能无法知道线程已经收到取消或停止请求。",[14,1898,1900],{"id":1899},"十三常见错误做法","十三、常见错误做法",[473,1902,1904],{"id":1903},"_1-捕获后什么都不做","1. 捕获后什么都不做",[144,1906,1908],{"className":197,"code":1907,"language":199,"meta":150,"style":150},"try {\n    process();\n} catch (Exception exception) {\n}\n",[22,1909,1910,1916,1922,1935],{"__ignoreMap":150},[203,1911,1912,1914],{"class":205,"line":206},[203,1913,245],{"class":209},[203,1915,248],{"class":219},[203,1917,1918,1920],{"class":205,"line":251},[203,1919,1114],{"class":215},[203,1921,257],{"class":219},[203,1923,1924,1926,1928,1931,1933],{"class":205,"line":260},[203,1925,263],{"class":219},[203,1927,28],{"class":209},[203,1929,1930],{"class":219}," (Exception ",[203,1932,272],{"class":271},[203,1934,275],{"class":219},[203,1936,1937],{"class":205,"line":278},[203,1938,288],{"class":219},[18,1940,1941],{},"这会吞掉失败信息，使调用方误以为操作成功。",[473,1943,1945],{"id":1944},"_2-到处捕获-exception","2. 到处捕获 Exception",[144,1947,1949],{"className":197,"code":1948,"language":199,"meta":150,"style":150},"catch (Exception exception) {\n    return null;\n}\n",[22,1950,1951,1961,1969],{"__ignoreMap":150},[203,1952,1953,1955,1957,1959],{"class":205,"line":206},[203,1954,28],{"class":209},[203,1956,1930],{"class":219},[203,1958,272],{"class":271},[203,1960,275],{"class":219},[203,1962,1963,1965,1967],{"class":205,"line":251},[203,1964,455],{"class":209},[203,1966,719],{"class":718},[203,1968,1297],{"class":219},[203,1970,1971],{"class":205,"line":260},[203,1972,288],{"class":219},[18,1974,1975],{},"过宽的捕获范围可能把编程错误、数据错误和外部系统失败混在一起，也容易让空值在后续代码中造成二次异常。",[473,1977,1979],{"id":1978},"_3-重复记录同一个异常","3. 重复记录同一个异常",[18,1981,1982],{},"如果每一层都记录日志后继续抛出，同一次失败可能产生多条重复堆栈。通常在真正处理异常或系统边界处完整记录一次即可。",[473,1984,1986],{"id":1985},"_4-用异常控制普通业务流程","4. 用异常控制普通业务流程",[18,1988,1989],{},"异常对象通常需要收集调用栈。对于可以通过正常条件判断表达的高频分支，不要依赖反复抛出异常完成流程跳转。",[14,1991,1993],{"id":1992},"十四常见问题速查","十四、常见问题速查",[299,1995,1996,2006],{},[302,1997,1998],{},[305,1999,2000,2003],{},[308,2001,2002],{},"问题",[308,2004,2005],{},"结论",[315,2007,2008,2024,2032,2040,2048,2058],{},[305,2009,2010,2018],{},[320,2011,2012,2014,2015,2017],{},[22,2013,39],{}," 是 ",[22,2016,43],{}," 吗？",[320,2019,2020,2021,2023],{},"不是，它们都是 ",[22,2022,32],{}," 的直接分支",[305,2025,2026,2029],{},[320,2027,2028],{},"运行时异常是否只能在运行时发现？",[320,2030,2031],{},"名字来自继承体系，不代表其他异常不在运行时发生",[305,2033,2034,2037],{},[320,2035,2036],{},"非受检异常能不能捕获？",[320,2038,2039],{},"可以，只是编译器不强制捕获",[305,2041,2042,2045],{},[320,2043,2044],{},"受检异常能否转换为运行时异常？",[320,2046,2047],{},"可以，但应保留 cause，并确保符合接口语义",[305,2049,2050,2055],{},[320,2051,2052,2054],{},[22,2053,1279],{}," 一定执行吗？",[320,2056,2057],{},"正常异常传播和方法返回时通常执行，但 JVM 强制退出、进程崩溃等情况下不能保证",[305,2059,2060,2065],{},[320,2061,1810,2062,2064],{},[22,2063,32],{}," 是否合适？",[320,2066,2067],{},"业务代码通常不合适，因为会同时捕获普通异常和严重 Error",[14,2069,2071],{"id":2070},"十五参考资料","十五、参考资料",[159,2073,2074,2082,2089,2096,2103],{},[162,2075,2076],{},[119,2077,2081],{"href":2078,"rel":2079},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F25\u002Fdocs\u002Fapi\u002Fjava.base\u002Fjava\u002Flang\u002FThrowable.html",[2080],"nofollow","Oracle Java API：Throwable",[162,2083,2084],{},[119,2085,2088],{"href":2086,"rel":2087},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F25\u002Fdocs\u002Fapi\u002Fjava.base\u002Fjava\u002Flang\u002FError.html",[2080],"Oracle Java API：Error",[162,2090,2091],{},[119,2092,2095],{"href":2093,"rel":2094},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F25\u002Fdocs\u002Fapi\u002Fjava.base\u002Fjava\u002Flang\u002FRuntimeException.html",[2080],"Oracle Java API：RuntimeException",[162,2097,2098],{},[119,2099,2102],{"href":2100,"rel":2101},"https:\u002F\u002Fdocs.oracle.com\u002Fjavase\u002Ftutorial\u002Fessential\u002Fexceptions\u002FcatchOrDeclare.html",[2080],"Oracle Java Tutorial：Catch or Specify Requirement",[162,2104,2105],{},[119,2106,2109],{"href":2107,"rel":2108},"https:\u002F\u002Fdocs.oracle.com\u002Fjavase\u002Ftutorial\u002Fessential\u002Fexceptions\u002Fcatch.html",[2080],"Oracle Java Tutorial：Catching and Handling Exceptions",[2111,2112,2113],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":150,"searchDepth":251,"depth":251,"links":2115},[2116,2117,2118,2119,2120,2121,2122,2126,2127,2128,2129,2130,2131,2132,2136,2137,2143,2144],{"id":16,"depth":251,"text":16},{"id":115,"depth":251,"text":115},{"id":132,"depth":251,"text":132},{"id":135,"depth":251,"text":136},{"id":291,"depth":251,"text":292},{"id":381,"depth":251,"text":382},{"id":410,"depth":251,"text":411,"children":2123},[2124,2125],{"id":475,"depth":260,"text":476},{"id":554,"depth":260,"text":555},{"id":674,"depth":251,"text":675},{"id":919,"depth":251,"text":920},{"id":1018,"depth":251,"text":1019},{"id":1092,"depth":251,"text":1093},{"id":1273,"depth":251,"text":1274},{"id":1444,"depth":251,"text":1445},{"id":1644,"depth":251,"text":1645,"children":2133},[2134,2135],{"id":1651,"depth":260,"text":1652},{"id":1721,"depth":260,"text":1722},{"id":1806,"depth":251,"text":1807},{"id":1899,"depth":251,"text":1900,"children":2138},[2139,2140,2141,2142],{"id":1903,"depth":260,"text":1904},{"id":1944,"depth":260,"text":1945},{"id":1978,"depth":260,"text":1979},{"id":1985,"depth":260,"text":1986},{"id":1992,"depth":251,"text":1993},{"id":2070,"depth":251,"text":2071},"wiki:java:java-exception-types","从 Throwable 继承体系出发，讲清 Error、Exception、RuntimeException、受检异常与非受检异常的区别，并介绍常见异常、捕获规则和自定义异常。","basic",false,"md",{},86,"\u002Fjava\u002Fjava-exception-types","面试专题",{"title":5,"description":2146},"java\u002Fjava-exception-types","2026-08-03","lBb2YfhMD4xVHK5yZHRtrjX4ymYovowSJkw2Q7Y4H1E",[2159,3676,4592,5898,6944,8306,10061,11209,15951,17148,19207,21904,23316,25256,27513,29197,31719],{"id":2160,"title":2161,"body":2162,"commentId":3665,"description":3666,"difficulty":3667,"draft":2148,"extension":2149,"meta":3668,"navigation":756,"order":3669,"path":3670,"section":3671,"seo":3672,"stem":3673,"updated":3674,"__hash__":3675},"java\u002Fjava\u002Fjdk8-synchronized-lock-states.md","JDK 8 synchronized 设计与锁状态转换",{"type":7,"value":2163,"toc":3608},[2164,2168,2174,2185,2188,2194,2197,2199,2208,2213,2216,2221,2227,2231,2234,2240,2247,2250,2256,2259,2262,2310,2314,2317,2323,2329,2332,2341,2346,2350,2357,2372,2486,2492,2498,2501,2504,2510,2513,2517,2523,2526,2532,2539,2542,2575,2579,2582,2585,2588,2592,2595,2598,2605,2608,2613,2617,2620,2626,2629,2632,2635,2641,2644,2647,2650,2656,2659,2665,2671,2675,2682,2685,2691,2694,2706,2709,2712,2715,2721,2724,2727,2730,2734,2737,2740,2744,2747,2751,2754,2758,2761,2764,2769,2773,2776,2778,2781,2784,2790,2793,2796,2799,2803,2817,2820,2826,2829,2835,2838,2844,2847,2851,2854,2858,2861,2864,2867,2873,2879,2883,2886,2892,2895,2901,2904,2907,2911,2914,2928,2931,2934,2940,2943,2949,2953,2960,2966,2969,2975,2978,2981,2995,2998,3002,3007,3013,3019,3028,3033,3037,3040,3043,3059,3062,3065,3068,3072,3248,3252,3256,3262,3266,3269,3275,3278,3282,3288,3292,3295,3366,3369,3467,3470,3496,3499,3505,3508,3514,3518,3522,3525,3529,3532,3536,3539,3543,3546,3550,3553,3557,3560,3564,3594,3597,3605],[14,2165,2167],{"id":2166},"一先给出整体结论","一、先给出整体结论",[18,2169,2170,2173],{},[22,2171,2172],{},"synchronized"," 是 Java 语言提供的内置同步机制。它在语义上负责三件事：",[159,2175,2176,2179,2182],{},[162,2177,2178],{},"同一时刻只允许满足条件的线程进入临界区；",[162,2180,2181],{},"支持同一线程重复获取同一把锁，也就是可重入；",[162,2183,2184],{},"解锁 happens-before 后续对同一 Monitor 的加锁，保证相关共享数据的可见性和有序性。",[18,2186,2187],{},"在 JDK 8 HotSpot 中，为了避免所有同步操作都直接阻塞线程，JVM 会根据竞争情况使用不同实现：",[144,2189,2192],{"className":2190,"code":2191,"language":149,"meta":150},[147],"偏向锁：同一个线程反复进入，尽量避免原子操作\n轻量级锁：线程交替执行或短暂竞争，使用栈上 Lock Record 和 CAS\n重量级锁：竞争较强或必须使用 Monitor 能力时，通过 ObjectMonitor 管理等待线程\n",[22,2193,2191],{"__ignoreMap":150},[18,2195,2196],{},"通常所说的“无锁、偏向锁、轻量级锁、重量级锁”，是对对象头及其关联同步结构的概括，不是 Java 语言规范定义的四种锁类型。",[14,2198,115],{"id":115},[18,2200,2201],{},[119,2202,2205],{"href":2203,"target":122,"rel":2204},"\u002Fimages\u002Fwiki\u002Fjava\u002Fjdk8-synchronized-state-transitions.svg",[124,125],[127,2206],{"src":2203,"alt":2207},"JDK 8 synchronized 锁状态转换总览",[105,2209,2210],{},[18,2211,2212],{},"图中信息较多，可点击图片查看原图。",[18,2214,2215],{},"需要先纠正一个常见说法：",[105,2217,2218],{},[18,2219,2220],{},"“锁只能升级，不能降级”适合帮助理解竞争路径，但并不严谨。",[18,2222,2223,2224],{},"轻量级锁正常释放后会通过 CAS 恢复对象原来的 Mark Word；已经膨胀的 ObjectMonitor 也可能在后续安全点被 JVM 回收。更准确的说法是：",[110,2225,2226],{},"持锁竞争过程中通常不会为了当前请求立即从重量级锁退回轻量级锁，但对象状态并非终身只能单向变化。",[14,2228,2230],{"id":2229},"二synchronized-在字节码中的入口","二、synchronized 在字节码中的入口",[18,2232,2233],{},"同步代码块通常编译为：",[144,2235,2238],{"className":2236,"code":2237,"language":149,"meta":150},[147],"monitorenter\n   临界区代码\nmonitorexit\n",[22,2239,2237],{"__ignoreMap":150},[18,2241,2242,2243,2246],{},"编译器会为正常返回和异常退出路径生成对应的 ",[22,2244,2245],{},"monitorexit","，保证退出同步块时释放锁。",[18,2248,2249],{},"同步方法不会在方法体中出现这两个指令，而是在方法访问标志中使用：",[144,2251,2254],{"className":2252,"code":2253,"language":149,"meta":150},[147],"ACC_SYNCHRONIZED\n",[22,2255,2253],{"__ignoreMap":150},[18,2257,2258],{},"JVM 在调用和返回同步方法时完成 Monitor 的进入与退出。",[18,2260,2261],{},"无论入口是哪一种，最终都围绕同一个锁对象工作：",[299,2263,2264,2274],{},[302,2265,2266],{},[305,2267,2268,2271],{},[308,2269,2270],{},"写法",[308,2272,2273],{},"实际锁对象",[315,2275,2276,2287,2299],{},[305,2277,2278,2281],{},[320,2279,2280],{},"实例同步方法",[320,2282,2283,2284],{},"当前实例 ",[22,2285,2286],{},"this",[305,2288,2289,2292],{},[320,2290,2291],{},"静态同步方法",[320,2293,2294,2295,2298],{},"当前类对应的 ",[22,2296,2297],{},"Class"," 对象",[305,2300,2301,2304],{},[320,2302,2303],{},"同步代码块",[320,2305,2306,2309],{},[22,2307,2308],{},"synchronized (...)"," 中指定的对象",[14,2311,2313],{"id":2312},"三对象头与-mark-word","三、对象头与 Mark Word",[18,2315,2316],{},"普通 Java 对象的对象头主要包含：",[144,2318,2321],{"className":2319,"code":2320,"language":149,"meta":150},[147],"Mark Word\nKlass Pointer\n",[22,2322,2320],{"__ignoreMap":150},[18,2324,2325,2326,2328],{},"数组对象还会额外保存数组长度。",[22,2327,2172],{}," 的关键状态主要编码在 Mark Word 中。",[18,2330,2331],{},"以 JDK 8、64 位 HotSpot 为例，Mark Word 会复用同一段空间：",[18,2333,2334],{},[119,2335,2338],{"href":2336,"target":122,"rel":2337},"\u002Fimages\u002Fwiki\u002Fjava\u002Fjdk8-synchronized-mark-word.svg",[124,125],[127,2339],{"src":2336,"alt":2340},"JDK 8 HotSpot Mark Word 与同步结构",[105,2342,2343],{},[18,2344,2345],{},"可点击图片查看完整尺寸。",[473,2347,2349],{"id":2348},"统一按最低-3-位阅读","统一按最低 3 位阅读",[18,2351,2352,2353,2356],{},"为了避免一会儿看 2 位、一会儿看 3 位，本文后续统一展示 Mark Word 的最低 ",[110,2354,2355],{},"3 位","。但要先区分“展示宽度”和“字段含义”：",[159,2358,2359,2366,2369],{},[162,2360,2361,2362,2365],{},"真正的锁标志始终是最低 ",[110,2363,2364],{},"2 位","；",[162,2367,2368],{},"只有普通未锁定和偏向布局会把倒数第 3 位解释成“偏向位”；",[162,2370,2371],{},"轻量级锁、重量级锁保存的是对齐后的指针，本文把其最低 3 位也完整写出，但倒数第 3 位不再表示偏向。",[299,2373,2374,2390],{},[302,2375,2376],{},[305,2377,2378,2381,2384,2387],{},[308,2379,2380],{},"最低 3 位",[308,2382,2383],{},"最低 2 位锁标志",[308,2385,2386],{},"状态",[308,2388,2389],{},"倒数第 3 位如何解释",[315,2391,2392,2413,2432,2450,2468],{},[305,2393,2394,2399,2404,2407],{},[320,2395,2396],{},[22,2397,2398],{},"001",[320,2400,2401],{},[22,2402,2403],{},"01",[320,2405,2406],{},"普通未锁定",[320,2408,2409,2410],{},"偏向位为 ",[22,2411,2412],{},"0",[305,2414,2415,2420,2424,2427],{},[320,2416,2417],{},[22,2418,2419],{},"101",[320,2421,2422],{},[22,2423,2403],{},[320,2425,2426],{},"可偏向或已偏向",[320,2428,2409,2429],{},[22,2430,2431],{},"1",[305,2433,2434,2439,2444,2447],{},[320,2435,2436],{},[22,2437,2438],{},"000",[320,2440,2441],{},[22,2442,2443],{},"00",[320,2445,2446],{},"轻量级锁",[320,2448,2449],{},"对齐后的 Lock Record 指针低位，不是偏向位",[305,2451,2452,2457,2462,2465],{},[320,2453,2454],{},[22,2455,2456],{},"010",[320,2458,2459],{},[22,2460,2461],{},"10",[320,2463,2464],{},"重量级锁",[320,2466,2467],{},"对齐后的 ObjectMonitor 指针低位，不是偏向位",[305,2469,2470,2475,2480,2483],{},[320,2471,2472],{},[22,2473,2474],{},"011",[320,2476,2477],{},[22,2478,2479],{},"11",[320,2481,2482],{},"GC 标记",[320,2484,2485],{},"标记状态的一部分，不是偏向位",[18,2487,2488,2489,2491],{},"因此 ",[22,2490,2419],{}," 应当拆开读成：",[144,2493,2496],{"className":2494,"code":2495,"language":149,"meta":150},[147],"偏向位 1 ｜ 锁标志 01\n",[22,2497,2495],{"__ignoreMap":150},[18,2499,2500],{},"它不是一个三位的“锁标志”。本文使用三位只是为了让所有状态在图表中对齐、便于比较。",[18,2502,2503],{},"其中偏向状态可以继续区分：",[144,2505,2508],{"className":2506,"code":2507,"language":149,"meta":150},[147],"匿名偏向：线程指针为空，允许第一个线程认领\n已偏向：线程指针指向获得偏向的 JavaThread\n",[22,2509,2507],{"__ignoreMap":150},[18,2511,2512],{},"Mark Word 是一块被复用的空间。例如普通未锁定对象可以直接保存 identity hash；偏向对象需要保存线程指针，因此二者不能同时使用同一布局。",[473,2514,2516],{"id":2515},"epoch-到底是什么","epoch 到底是什么",[18,2518,2519,2522],{},[22,2520,2521],{},"epoch"," 可以理解为某个类的“偏向版本号”，它不是时间戳，也不是对象年龄。",[18,2524,2525],{},"偏向锁不只依赖对象自身。支持偏向的类会在类的 prototype header 中保存一个当前 epoch；每个偏向对象的 Mark Word 中也会记录对象认领时使用的 epoch。线程进入偏向对象时，会比较两者：",[144,2527,2530],{"className":2528,"code":2529,"language":149,"meta":150},[147],"对象 epoch == 类当前 epoch\n    → 这份偏向记录仍属于当前版本，继续检查偏向线程\n\n对象 epoch != 类当前 epoch\n    → 这份偏向记录已经过期，可以进入重偏向或撤销流程\n",[22,2531,2529],{"__ignoreMap":150},[18,2533,2534,2535,2538],{},"它主要服务于",[110,2536,2537],{},"批量重偏向","。当一批对象从线程 A 整体转交给线程 B 使用时，JVM 不必立即逐个改写所有对象头，而是先提升类的 epoch。旧对象之后被访问时，会因为 epoch 不匹配而按新版本重新处理。",[18,2540,2541],{},"所以需要把两个容易混淆的字段分开：",[299,2543,2544,2554],{},[302,2545,2546],{},[305,2547,2548,2551],{},[308,2549,2550],{},"字段",[308,2552,2553],{},"含义",[315,2555,2556,2566],{},[305,2557,2558,2563],{},[320,2559,2560],{},[22,2561,2562],{},"age",[320,2564,2565],{},"对象的 GC 年龄，用于分代回收",[305,2567,2568,2572],{},[320,2569,2570],{},[22,2571,2521],{},[320,2573,2574],{},"类的偏向版本，用于判断对象上的偏向记录是否过期",[14,2576,2578],{"id":2577},"四jdk-8-为什么需要三种锁实现","四、JDK 8 为什么需要三种锁实现",[18,2580,2581],{},"不同竞争场景适合不同成本的同步方式。",[473,2583,2584],{"id":2584},"只有一个线程反复进入",[18,2586,2587],{},"如果一个对象长期只被同一个线程加锁，每次都执行 CAS 仍然是额外成本。偏向锁把对象与线程关联起来，后续同一线程进入时主要检查对象头是否仍偏向自己。",[473,2589,2591],{"id":2590},"多线程交替执行但没有同时竞争","多线程交替执行，但没有同时竞争",[18,2593,2594],{},"例如线程 A 使用完后线程 B 才进入。此时没有必要挂起线程，可以通过栈上 Lock Record 和 CAS 完成加锁、解锁。",[473,2596,2597],{"id":2597},"多线程同时争抢或需要等待队列",[18,2599,2600,2601,2604],{},"当 CAS 快速路径无法解决竞争，或者调用 ",[22,2602,2603],{},"wait()"," 需要条件等待集合时，JVM 使用 ObjectMonitor 管理 Owner、竞争线程和等待线程。",[18,2606,2607],{},"所以这三种实现的目标不是简单比较“谁更高级”，而是：",[105,2609,2610],{},[18,2611,2612],{},"根据实际竞争强度，在原子操作、CPU 自旋、线程阻塞和唤醒成本之间做权衡。",[14,2614,2616],{"id":2615},"五对象初始状态普通未锁定还是匿名偏向","五、对象初始状态：普通未锁定还是匿名偏向",[18,2618,2619],{},"JDK 8 默认启用偏向锁，但默认存在启动延迟：",[144,2621,2624],{"className":2622,"code":2623,"language":149,"meta":150},[147],"-XX:+UseBiasedLocking\n-XX:BiasedLockingStartupDelay=4000\n",[22,2625,2623],{"__ignoreMap":150},[18,2627,2628],{},"因此需要区分对象创建时机：",[473,2630,2631],{"id":2631},"偏向锁尚未启用或被关闭",[18,2633,2634],{},"新对象通常使用普通未锁定布局：",[144,2636,2639],{"className":2637,"code":2638,"language":149,"meta":150},[147],"[identity hash | age | 0 | 01]\n",[22,2640,2638],{"__ignoreMap":150},[18,2642,2643],{},"线程第一次进入同步块时，直接尝试建立轻量级锁。",[473,2645,2646],{"id":2646},"偏向锁已经启用",[18,2648,2649],{},"支持偏向的类创建新对象时，对象通常处于匿名偏向状态：",[144,2651,2654],{"className":2652,"code":2653,"language":149,"meta":150},[147],"[0 | epoch | age | 1 | 01]\n",[22,2655,2653],{"__ignoreMap":150},[18,2657,2658],{},"因此严格来说，不应把这条路径描述为：",[144,2660,2663],{"className":2661,"code":2662,"language":149,"meta":150},[147],"新对象先是无锁，然后第一次进入才“升级”为可偏向状态\n",[22,2664,2662],{"__ignoreMap":150},[18,2666,2667,2668],{},"更准确的说法是：",[110,2669,2670],{},"对象创建时便根据类的 prototype header 获得普通未锁定或匿名偏向的 Mark Word。",[14,2672,2674],{"id":2673},"六偏向锁的获取与重入","六、偏向锁的获取与重入",[18,2676,2677,2678,2681],{},"线程进入一个匿名偏向对象时，会尝试通过 CAS 把自己的 ",[22,2679,2680],{},"JavaThread*","、类当前的 epoch 等信息写入 Mark Word。",[18,2683,2684],{},"成功后，对象变成：",[144,2686,2689],{"className":2687,"code":2688,"language":149,"meta":150},[147],"[当前 JavaThread* | epoch | age | 1 | 01]\n",[22,2690,2688],{"__ignoreMap":150},[18,2692,2693],{},"同一个线程以后再次进入时，主要检查：",[2695,2696,2697,2700,2703],"ol",{},[162,2698,2699],{},"Mark Word 是否仍是偏向模式；",[162,2701,2702],{},"偏向线程是否是当前线程；",[162,2704,2705],{},"对象 epoch 是否仍与类的 prototype header 匹配。",[18,2707,2708],{},"全部满足时可直接进入，不需要再次通过 CAS 抢占锁。",[473,2710,2711],{"id":2711},"偏向锁退出为什么不清除线程信息",[18,2713,2714],{},"偏向锁针对的就是“同一个线程还会再次进入”这一场景。同步块结束后，Mark Word 通常仍保留对原线程的偏向：",[144,2716,2719],{"className":2717,"code":2718,"language":149,"meta":150},[147],"进入前：偏向线程 A\n执行中：偏向线程 A\n退出后：仍偏向线程 A\n",[22,2720,2718],{"__ignoreMap":150},[18,2722,2723],{},"它不是表示线程 A 永远占用临界区，而是表示下一次线程 A 再进入时可以走低成本路径。",[473,2725,2726],{"id":2726},"偏向锁如何实现可重入",[18,2728,2729],{},"对象头记录了偏向线程，但不通过一个公共计数器记录每次进入。JVM 可以结合当前线程栈上的锁记录识别同步嵌套。对同一偏向线程而言，重复进入不需要竞争性地修改对象头。",[14,2731,2733],{"id":2732},"七其他线程访问偏向对象时发生什么","七、其他线程访问偏向对象时发生什么",[18,2735,2736],{},"线程 B 遇到偏向线程 A 的对象时，不能简单地直接覆盖线程指针。JVM 需要先判断原偏向是否仍然有效。",[18,2738,2739],{},"可能出现以下路径。",[473,2741,2743],{"id":2742},"_1-epoch-已过期","1. epoch 已过期",[18,2745,2746],{},"类发生批量重偏向后，旧对象的 epoch 可能落后于类的 epoch。线程 B 可以尝试通过 CAS 将对象重新偏向自己。",[473,2748,2750],{"id":2749},"_2-原线程已经不再持有这把锁","2. 原线程已经不再持有这把锁",[18,2752,2753],{},"JVM 可以撤销原偏向，使对象恢复为普通未锁定状态，或者在允许的情况下重新偏向新线程。",[473,2755,2757],{"id":2756},"_3-原线程仍在同步块中","3. 原线程仍在同步块中",[18,2759,2760],{},"JVM 需要检查原线程栈，撤销偏向并重建合法的锁状态。如果此时存在实际竞争，后续会进入轻量级竞争或直接膨胀为 ObjectMonitor。",[18,2762,2763],{},"因此：",[105,2765,2766],{},[18,2767,2768],{},"另一个线程访问偏向对象，不等于必然直接升级为重量级锁。是否重偏向、撤销为普通状态、转换为轻量级锁或膨胀，需要结合 epoch、类级启发式统计和原线程是否仍持锁判断。",[14,2770,2772],{"id":2771},"八批量重偏向与批量撤销","八、批量重偏向与批量撤销",[18,2774,2775],{},"如果某个类的大量对象不断被不同线程使用，逐个在安全点撤销偏向的成本会很高。HotSpot 会对同一类的偏向撤销次数做启发式统计。",[473,2777,2537],{"id":2537},[18,2779,2780],{},"当撤销达到一定阈值时，JVM 可以提升类 prototype header 中的 epoch。旧对象不需要立刻逐个修改；之后线程发现对象 epoch 过期时，再尝试将它偏向当前线程。",[18,2782,2783],{},"适合这种模式：",[144,2785,2788],{"className":2786,"code":2787,"language":149,"meta":150},[147],"一批对象先由线程 A 使用\n之后整体交给线程 B 使用\n但同一时刻竞争并不强\n",[22,2789,2787],{"__ignoreMap":150},[473,2791,2792],{"id":2792},"批量撤销",[18,2794,2795],{},"如果该类持续表现出多线程竞争，偏向锁已经不再适合，JVM 可以撤销该类的偏向能力。之后新对象不再默认可偏向，已有对象也会按正常锁路径处理。",[18,2797,2798],{},"这也是为什么偏向锁不能只按“对象自身的四级升级”理解：它还存在以类为粒度的 prototype header、epoch 和批量启发式机制。",[14,2800,2802],{"id":2801},"九普通未锁定到轻量级锁","九、普通未锁定到轻量级锁",[18,2804,2805,2806,2809,2810,2813,2814,2816],{},"当对象处于普通未锁定状态，线程进入同步块时会在当前栈帧创建锁记录，HotSpot 源码中对应 ",[22,2807,2808],{},"BasicLock","，解释器栈中通常由 ",[22,2811,2812],{},"BasicObjectLock"," 将对象引用和 ",[22,2815,2808],{}," 组织在一起。",[18,2818,2819],{},"概念流程如下：",[144,2821,2824],{"className":2822,"code":2823,"language":149,"meta":150},[147],"1. 在线程栈创建 Lock Record\n2. 将对象原 Mark Word 保存到 displaced header\n3. CAS 修改对象 Mark Word\n4. 让 Mark Word 指向当前 Lock Record\n",[22,2825,2823],{"__ignoreMap":150},[18,2827,2828],{},"CAS 成功后：",[144,2830,2833],{"className":2831,"code":2832,"language":149,"meta":150},[147],"对象 Mark Word  →  线程栈 Lock Record\nLock Record     →  保存原 Mark Word\n",[22,2834,2832],{"__ignoreMap":150},[18,2836,2837],{},"对象头低两位为：",[144,2839,2842],{"className":2840,"code":2841,"language":149,"meta":150},[147],"00\n",[22,2843,2841],{"__ignoreMap":150},[18,2845,2846],{},"这就是通常所说的轻量级锁或栈锁。",[473,2848,2850],{"id":2849},"为什么要保存-displaced-header","为什么要保存 displaced header",[18,2852,2853],{},"轻量级锁占用了对象原来的 Mark Word。解锁时需要把原 Mark Word 恢复回对象头，因此先把它保存到 Lock Record。",[14,2855,2857],{"id":2856},"十轻量级锁的可重入","十、轻量级锁的可重入",[18,2859,2860],{},"同一个线程再次获取已经由自己栈锁定的对象时，JVM 会识别对象头中的 Lock Record 指针属于当前线程栈。",[18,2862,2863],{},"此时新的 Lock Record 会使用特殊值表示递归进入，例如 displaced header 置空，而不是再次覆盖对象头。",[18,2865,2866],{},"退出时：",[144,2868,2871],{"className":2869,"code":2870,"language":149,"meta":150},[147],"递归层 Lock Record：只退出当前递归层\n最外层 Lock Record：负责真正恢复对象头\n",[22,2872,2870],{"__ignoreMap":150},[18,2874,2875,2876,2878],{},"这解释了为什么 ",[22,2877,2172],{}," 天然支持可重入。",[14,2880,2882],{"id":2881},"十一轻量级锁如何释放","十一、轻量级锁如何释放",[18,2884,2885],{},"最外层同步块退出时，线程使用 CAS 尝试把 Lock Record 中保存的 displaced header 恢复到对象 Mark Word：",[144,2887,2890],{"className":2888,"code":2889,"language":149,"meta":150},[147],"期望值：对象头仍指向当前 Lock Record\n新值：进入同步块前保存的原 Mark Word\n",[22,2891,2889],{"__ignoreMap":150},[18,2893,2894],{},"CAS 成功：",[144,2896,2899],{"className":2897,"code":2898,"language":149,"meta":150},[147],"轻量级锁正常释放\n对象恢复普通未锁定状态\n",[22,2900,2898],{"__ignoreMap":150},[18,2902,2903],{},"CAS 失败通常意味着对象已在竞争过程中膨胀，不能再直接把原对象头覆盖回去，需要走 ObjectMonitor 的退出逻辑。",[18,2905,2906],{},"这就是“锁只能升级不能降级”表述不够准确的直接例子：没有发生膨胀的轻量级锁，退出后会恢复原 Mark Word。",[14,2908,2910],{"id":2909},"十二轻量级锁何时膨胀","十二、轻量级锁何时膨胀",[18,2912,2913],{},"线程尝试用 CAS 建立轻量级锁失败，说明对象头已经发生变化。可能原因包括：",[159,2915,2916,2919,2922,2925],{},[162,2917,2918],{},"另一个线程持有轻量级锁；",[162,2920,2921],{},"对象已经膨胀为 ObjectMonitor；",[162,2923,2924],{},"竞争期间其他线程正在处理锁状态；",[162,2926,2927],{},"必须执行依赖 ObjectMonitor 的操作。",[18,2929,2930],{},"JVM 可以先进行短暂自旋，希望持锁线程很快退出。自旋避免了线程立即挂起和恢复，但会消耗 CPU，因此只适合临界区较短的情况。",[18,2932,2933],{},"当快速路径不能解决问题时，JVM 会执行 Monitor inflation：",[144,2935,2938],{"className":2936,"code":2937,"language":149,"meta":150},[147],"创建或取得 ObjectMonitor\n复制并保存原 Mark Word\n将对象头改为指向 ObjectMonitor\n由 ObjectMonitor 负责后续竞争\n",[22,2939,2937],{"__ignoreMap":150},[18,2941,2942],{},"对象头低两位变成：",[144,2944,2947],{"className":2945,"code":2946,"language":149,"meta":150},[147],"10\n",[22,2948,2946],{"__ignoreMap":150},[14,2950,2952],{"id":2951},"十三重量级锁与-objectmonitor","十三、重量级锁与 ObjectMonitor",[18,2954,2955,2956,2959],{},"重量级状态下，对象 Mark Word 指向一个 ",[22,2957,2958],{},"ObjectMonitor","。理解它时重点关注以下字段或逻辑角色：",[144,2961,2964],{"className":2962,"code":2963,"language":149,"meta":150},[147],"_owner       当前持有 Monitor 的线程或相关锁记录\n_recursions  重入次数\n_cxq         新到达竞争线程形成的竞争队列\n_EntryList   等待重新竞争 Owner 的线程\n_WaitSet     调用 wait 后等待通知的线程\n_header      膨胀前保存的对象 Mark Word\n",[22,2965,2963],{"__ignoreMap":150},[18,2967,2968],{},"概念结构：",[144,2970,2973],{"className":2971,"code":2972,"language":149,"meta":150},[147],"对象 Mark Word\n      │\n      ▼\nObjectMonitor\n  ├─ Owner\n  ├─ Recursions\n  ├─ cxq \u002F EntryList\n  ├─ WaitSet\n  └─ displaced header\n",[22,2974,2972],{"__ignoreMap":150},[18,2976,2977],{},"竞争线程可能先自旋尝试获得 Owner。仍然失败时，线程会进入等待结构并被挂起；持有线程退出后，Monitor 选择或唤醒后继线程继续竞争。",[18,2979,2980],{},"“重量级”的成本主要来自：",[159,2982,2983,2986,2989,2992],{},[162,2984,2985],{},"维护竞争和等待队列；",[162,2987,2988],{},"线程阻塞、唤醒与调度；",[162,2990,2991],{},"高竞争下的上下文切换；",[162,2993,2994],{},"对 Monitor 状态的原子协调。",[18,2996,2997],{},"它并不意味着每次操作都必然立刻执行一次昂贵的系统调用，HotSpot 仍包含快速路径和自适应自旋等优化。",[14,2999,3001],{"id":3000},"十四waitnotify-为什么会涉及重量级-monitor","十四、wait、notify 为什么会涉及重量级 Monitor",[18,3003,3004,3006],{},[22,3005,2603],{}," 的语义不是普通锁竞争：",[144,3008,3011],{"className":3009,"code":3010,"language":149,"meta":150},[147],"确认当前线程持有 Monitor\n完整释放锁及其重入层数\n进入 WaitSet\n等待 notify、notifyAll、中断或超时\n重新参与锁竞争\n恢复原重入状态后返回\n",[22,3012,3010],{"__ignoreMap":150},[18,3014,3015,3016,3018],{},"这需要 Owner、WaitSet、EntryList、重入次数等完整 Monitor 能力，因此 JDK 8 HotSpot 执行 ",[22,3017,2603],{}," 时会确保对象已经膨胀为 ObjectMonitor。",[18,3020,3021,40,3024,3027],{},[22,3022,3023],{},"notify()",[22,3025,3026],{},"notifyAll()"," 存在一个细节：如果对象当前只是由调用线程栈锁定，那么它不可能已经拥有 WaitSet，HotSpot 可以直接返回而不做无意义的膨胀；否则会取得或膨胀 ObjectMonitor，再处理等待线程。",[18,3029,3030,3032],{},[22,3031,3023],{}," 只是把等待线程从“等待条件”推进到“可以重新竞争锁”的阶段，并不会让它绕过当前 Owner 立即执行。",[14,3034,3036],{"id":3035},"十五identityhashcode-对锁状态的影响","十五、identityHashCode 对锁状态的影响",[18,3038,3039],{},"普通未锁定对象可以把 identity hash 保存在 Mark Word 中，但偏向锁的 Mark Word 需要保存 JavaThread 指针。",[18,3041,3042],{},"因此，对偏向对象计算：",[144,3044,3046],{"className":197,"code":3045,"language":199,"meta":150,"style":150},"System.identityHashCode(obj);\n",[22,3047,3048],{"__ignoreMap":150},[203,3049,3050,3053,3056],{"class":205,"line":206},[203,3051,3052],{"class":219},"System.",[203,3054,3055],{"class":215},"identityHashCode",[203,3057,3058],{"class":219},"(obj);\n",[18,3060,3061],{},"通常会导致偏向撤销，使对象头能够保存 hash。",[18,3063,3064],{},"如果对象正在使用轻量级锁，原 Mark Word 已经保存在当前线程栈的 Lock Record 中。为了稳定保存 identity hash，并让其他线程可靠读取，HotSpot 的相关路径可能把对象膨胀为 ObjectMonitor，再把 hash 保存在 Monitor 保存的 header 中。",[18,3066,3067],{},"所以观察锁升级实验时，不要在不知情的情况下调用可能计算 identity hash 的方法，否则实验本身会改变对象头。",[14,3069,3071],{"id":3070},"十六完整状态转换表","十六、完整状态转换表",[299,3073,3074,3087],{},[302,3075,3076],{},[305,3077,3078,3081,3084],{},[308,3079,3080],{},"当前状态",[308,3082,3083],{},"触发条件",[308,3085,3086],{},"可能结果",[315,3088,3089,3102,3115,3128,3140,3152,3164,3177,3189,3201,3213,3224,3237],{},[305,3090,3091,3096,3099],{},[320,3092,3093,3094],{},"普通未锁定 ",[22,3095,2398],{},[320,3097,3098],{},"首次进入同步块",[320,3100,3101],{},"CAS 成功后成为轻量级锁",[305,3103,3104,3109,3112],{},[320,3105,3106,3107],{},"匿名偏向 ",[22,3108,2419],{},[320,3110,3111],{},"第一个线程进入",[320,3113,3114],{},"CAS 写入线程指针，成为已偏向状态",[305,3116,3117,3122,3125],{},[320,3118,3119,3120],{},"已偏向 ",[22,3121,2419],{},[320,3123,3124],{},"原线程再次进入",[320,3126,3127],{},"保持偏向，低成本重入",[305,3129,3130,3134,3137],{},[320,3131,3119,3132],{},[22,3133,2419],{},[320,3135,3136],{},"新线程访问且 epoch 过期",[320,3138,3139],{},"尝试重偏向或撤销",[305,3141,3142,3146,3149],{},[320,3143,3119,3144],{},[22,3145,2419],{},[320,3147,3148],{},"新线程访问，原线程未持锁",[320,3150,3151],{},"撤销为普通状态，或按策略重偏向",[305,3153,3154,3158,3161],{},[320,3155,3119,3156],{},[22,3157,2419],{},[320,3159,3160],{},"新线程访问，原线程仍持锁",[320,3162,3163],{},"撤销偏向，转轻量级或膨胀",[305,3165,3166,3171,3174],{},[320,3167,3168,3169],{},"轻量级 ",[22,3170,2443],{},[320,3172,3173],{},"当前线程重入",[320,3175,3176],{},"新增递归 Lock Record，保持轻量级",[305,3178,3179,3183,3186],{},[320,3180,3168,3181],{},[22,3182,2443],{},[320,3184,3185],{},"无竞争退出",[320,3187,3188],{},"CAS 恢复原 Mark Word",[305,3190,3191,3195,3198],{},[320,3192,3168,3193],{},[22,3194,2443],{},[320,3196,3197],{},"竞争持续或恢复对象头失败",[320,3199,3200],{},"膨胀为 ObjectMonitor",[305,3202,3203,3206,3211],{},[320,3204,3205],{},"任意适用状态",[320,3207,3208,3210],{},[22,3209,2603],{}," 等需要完整 Monitor 语义",[320,3212,3200],{},[305,3214,3215,3218,3221],{},[320,3216,3217],{},"偏向\u002F轻量级",[320,3219,3220],{},"计算 identity hash 等特殊场景",[320,3222,3223],{},"撤销偏向或膨胀",[305,3225,3226,3231,3234],{},[320,3227,3228,3229],{},"重量级 ",[22,3230,2461],{},[320,3232,3233],{},"Owner 退出",[320,3235,3236],{},"唤醒\u002F选择后继；对象仍可保持膨胀",[305,3238,3239,3242,3245],{},[320,3240,3241],{},"空闲 ObjectMonitor",[320,3243,3244],{},"后续安全点清理",[320,3246,3247],{},"JVM 可能执行 Monitor deflation",[14,3249,3251],{"id":3250},"十七三条典型执行路径","十七、三条典型执行路径",[473,3253,3255],{"id":3254},"路径一同一个线程反复进入","路径一：同一个线程反复进入",[144,3257,3260],{"className":3258,"code":3259,"language":149,"meta":150},[147],"匿名偏向\n  → CAS 偏向线程 A\n  → A 执行同步块\n  → A 退出但保留偏向\n  → A 再次进入，快速命中\n",[22,3261,3259],{"__ignoreMap":150},[473,3263,3265],{"id":3264},"路径二线程交替使用没有重叠竞争","路径二：线程交替使用，没有重叠竞争",[18,3267,3268],{},"偏向关闭或偏向已撤销时：",[144,3270,3273],{"className":3271,"code":3272,"language":149,"meta":150},[147],"普通未锁定\n  → A 建立轻量级锁\n  → A CAS 恢复对象头\n  → B 建立轻量级锁\n  → B CAS 恢复对象头\n",[22,3274,3272],{"__ignoreMap":150},[18,3276,3277],{},"这种场景没有必要让线程进入阻塞等待。",[473,3279,3281],{"id":3280},"路径三多线程同时竞争","路径三：多线程同时竞争",[144,3283,3286],{"className":3284,"code":3285,"language":149,"meta":150},[147],"A 持有轻量级锁\n  → B CAS 失败并短暂自旋\n  → A 未及时释放\n  → 对象膨胀为 ObjectMonitor\n  → B 进入竞争\u002F等待结构\n  → A 退出并推进后继竞争\n",[22,3287,3285],{"__ignoreMap":150},[14,3289,3291],{"id":3290},"十八jol-验证时要注意什么","十八、JOL 验证时要注意什么",[18,3293,3294],{},"可以使用 JOL 查看对象布局：",[144,3296,3300],{"className":3297,"code":3298,"language":3299,"meta":150,"style":150},"language-xml shiki shiki-themes github-light github-dark","\u003Cdependency>\n    \u003CgroupId>org.openjdk.jol\u003C\u002FgroupId>\n    \u003CartifactId>jol-core\u003C\u002FartifactId>\n    \u003Cversion>0.17\u003C\u002Fversion>\n\u003C\u002Fdependency>\n","xml",[22,3301,3302,3314,3329,3343,3357],{"__ignoreMap":150},[203,3303,3304,3307,3311],{"class":205,"line":206},[203,3305,3306],{"class":219},"\u003C",[203,3308,3310],{"class":3309},"s9eBZ","dependency",[203,3312,3313],{"class":219},">\n",[203,3315,3316,3319,3322,3325,3327],{"class":205,"line":251},[203,3317,3318],{"class":219},"    \u003C",[203,3320,3321],{"class":3309},"groupId",[203,3323,3324],{"class":219},">org.openjdk.jol\u003C\u002F",[203,3326,3321],{"class":3309},[203,3328,3313],{"class":219},[203,3330,3331,3333,3336,3339,3341],{"class":205,"line":260},[203,3332,3318],{"class":219},[203,3334,3335],{"class":3309},"artifactId",[203,3337,3338],{"class":219},">jol-core\u003C\u002F",[203,3340,3335],{"class":3309},[203,3342,3313],{"class":219},[203,3344,3345,3347,3350,3353,3355],{"class":205,"line":278},[203,3346,3318],{"class":219},[203,3348,3349],{"class":3309},"version",[203,3351,3352],{"class":219},">0.17\u003C\u002F",[203,3354,3349],{"class":3309},[203,3356,3313],{"class":219},[203,3358,3359,3362,3364],{"class":205,"line":285},[203,3360,3361],{"class":219},"\u003C\u002F",[203,3363,3310],{"class":3309},[203,3365,3313],{"class":219},[18,3367,3368],{},"示例：",[144,3370,3372],{"className":197,"code":3371,"language":199,"meta":150,"style":150},"Object lock = new Object();\n\nSystem.out.println(ClassLayout.parseInstance(lock).toPrintable());\n\nsynchronized (lock) {\n    System.out.println(ClassLayout.parseInstance(lock).toPrintable());\n}\n\nSystem.out.println(ClassLayout.parseInstance(lock).toPrintable());\n",[22,3373,3374,3388,3392,3415,3419,3426,3443,3447,3451],{"__ignoreMap":150},[203,3375,3376,3379,3381,3383,3386],{"class":205,"line":206},[203,3377,3378],{"class":219},"Object lock ",[203,3380,495],{"class":209},[203,3382,212],{"class":209},[203,3384,3385],{"class":215}," Object",[203,3387,257],{"class":219},[203,3389,3390],{"class":205,"line":251},[203,3391,757],{"emptyLinePlaceholder":756},[203,3393,3394,3397,3400,3403,3406,3409,3412],{"class":205,"line":260},[203,3395,3396],{"class":219},"System.out.",[203,3398,3399],{"class":215},"println",[203,3401,3402],{"class":219},"(ClassLayout.",[203,3404,3405],{"class":215},"parseInstance",[203,3407,3408],{"class":219},"(lock).",[203,3410,3411],{"class":215},"toPrintable",[203,3413,3414],{"class":219},"());\n",[203,3416,3417],{"class":205,"line":278},[203,3418,757],{"emptyLinePlaceholder":756},[203,3420,3421,3423],{"class":205,"line":285},[203,3422,2172],{"class":209},[203,3424,3425],{"class":219}," (lock) {\n",[203,3427,3428,3431,3433,3435,3437,3439,3441],{"class":205,"line":541},[203,3429,3430],{"class":219},"    System.out.",[203,3432,3399],{"class":215},[203,3434,3402],{"class":219},[203,3436,3405],{"class":215},[203,3438,3408],{"class":219},[203,3440,3411],{"class":215},[203,3442,3414],{"class":219},[203,3444,3445],{"class":205,"line":549},[203,3446,288],{"class":219},[203,3448,3449],{"class":205,"line":787},[203,3450,757],{"emptyLinePlaceholder":756},[203,3452,3453,3455,3457,3459,3461,3463,3465],{"class":205,"line":799},[203,3454,3396],{"class":219},[203,3456,3399],{"class":215},[203,3458,3402],{"class":219},[203,3460,3405],{"class":215},[203,3462,3408],{"class":219},[203,3464,3411],{"class":215},[203,3466,3414],{"class":219},[18,3468,3469],{},"实验时要控制以下变量：",[159,3471,3472,3475,3481,3487,3490,3493],{},[162,3473,3474],{},"明确使用 JDK 8 HotSpot；",[162,3476,3477,3478,2365],{},"记录是否开启 ",[22,3479,3480],{},"UseBiasedLocking",[162,3482,3483,3484,2365],{},"记录 ",[22,3485,3486],{},"BiasedLockingStartupDelay",[162,3488,3489],{},"避免日志、调试器或工具意外计算 identity hash；",[162,3491,3492],{},"JOL 输出会受 32\u002F64 位、压缩指针和 GC 配置影响；",[162,3494,3495],{},"线程已经终止不代表任何场景都一定重偏向，仍要看 epoch 和撤销策略。",[18,3497,3498],{},"为了避免默认 4 秒延迟影响实验，可以显式设置：",[144,3500,3503],{"className":3501,"code":3502,"language":149,"meta":150},[147],"-XX:+UseBiasedLocking\n-XX:BiasedLockingStartupDelay=0\n",[22,3504,3502],{"__ignoreMap":150},[18,3506,3507],{},"关闭偏向锁、直接观察轻量级路径：",[144,3509,3512],{"className":3510,"code":3511,"language":149,"meta":150},[147],"-XX:-UseBiasedLocking\n",[22,3513,3511],{"__ignoreMap":150},[14,3515,3517],{"id":3516},"十九常见误区","十九、常见误区",[473,3519,3521],{"id":3520},"误区一新对象一定先是无锁再升级为偏向锁","误区一：新对象一定先是无锁，再升级为偏向锁",[18,3523,3524],{},"不准确。偏向能力启用后，支持偏向的类可以让新对象直接使用匿名偏向的 prototype header。",[473,3526,3528],{"id":3527},"误区二第二个线程出现就一定升级为重量级锁","误区二：第二个线程出现就一定升级为重量级锁",[18,3530,3531],{},"不准确。它可能触发重偏向、偏向撤销、轻量级锁，也可能在存在实际竞争时膨胀。",[473,3533,3535],{"id":3534},"误区三轻量级锁就是一直自旋","误区三：轻量级锁就是一直自旋",[18,3537,3538],{},"不准确。轻量级锁的核心是栈上 Lock Record 与对象头 CAS；自旋是竞争时可能采用的等待优化，不是轻量级锁的完整定义。",[473,3540,3542],{"id":3541},"误区四重量级锁完全由操作系统-mutex-等同实现","误区四：重量级锁完全由操作系统 Mutex 等同实现",[18,3544,3545],{},"过度简化。ObjectMonitor 是 HotSpot 的 JVM 级同步结构，内部会使用 CAS、自旋、队列、park\u002Funpark 等机制；线程最终阻塞和唤醒会依赖操作系统能力，但两者不能直接画等号。",[473,3547,3549],{"id":3548},"误区五锁一旦膨胀这个对象终身都是重量级锁","误区五：锁一旦膨胀，这个对象终身都是重量级锁",[18,3551,3552],{},"不准确。当前竞争阶段通常不会立即降级，但空闲 Monitor 后续可能由 JVM 在安全点执行 deflation。",[473,3554,3556],{"id":3555},"误区六这套流程适用于所有-jdk","误区六：这套流程适用于所有 JDK",[18,3558,3559],{},"不准确。本文讨论的是 JDK 8 HotSpot。偏向锁后来被默认禁用，现代 HotSpot 的锁实现也持续演进，因此理解具体机制时必须先限定版本。",[14,3561,3563],{"id":3562},"二十参考资料","二十、参考资料",[159,3565,3566,3573,3580,3587],{},[162,3567,3568],{},[119,3569,3572],{"href":3570,"rel":3571},"https:\u002F\u002Fgithub.com\u002Fopenjdk\u002Fjdk8u\u002Fblob\u002Fmaster\u002Fhotspot\u002Fsrc\u002Fshare\u002Fvm\u002Foops\u002FmarkOop.hpp",[2080],"JDK 8 HotSpot：markOop.hpp",[162,3574,3575],{},[119,3576,3579],{"href":3577,"rel":3578},"https:\u002F\u002Fgithub.com\u002Fopenjdk\u002Fjdk8u\u002Fblob\u002Fmaster\u002Fhotspot\u002Fsrc\u002Fshare\u002Fvm\u002Fruntime\u002Fsynchronizer.cpp",[2080],"JDK 8 HotSpot：synchronizer.cpp",[162,3581,3582],{},[119,3583,3586],{"href":3584,"rel":3585},"https:\u002F\u002Fgithub.com\u002Fopenjdk\u002Fjdk8u\u002Fblob\u002Fmaster\u002Fhotspot\u002Fsrc\u002Fshare\u002Fvm\u002Fruntime\u002FbiasedLocking.cpp",[2080],"JDK 8 HotSpot：biasedLocking.cpp",[162,3588,3589],{},[119,3590,3593],{"href":3591,"rel":3592},"https:\u002F\u002Fgithub.com\u002Fopenjdk\u002Fjdk8u\u002Fblob\u002Fmaster\u002Fhotspot\u002Fsrc\u002Fshare\u002Fvm\u002Fruntime\u002FobjectMonitor.hpp",[2080],"JDK 8 HotSpot：objectMonitor.hpp",[14,3595,3596],{"id":3596},"相关内容",[159,3598,3599],{},[162,3600,3601],{},[119,3602,3604],{"href":3603},"\u002Fwiki\u002Fjava\u002Fsynchronized-vs-reentrantlock\u002F","synchronized 和 ReentrantLock 的区别",[2111,3606,3607],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}",{"title":150,"searchDepth":251,"depth":251,"links":3609},[3610,3611,3612,3613,3617,3622,3626,3630,3635,3639,3642,3643,3644,3645,3646,3647,3648,3649,3654,3655,3663,3664],{"id":2166,"depth":251,"text":2167},{"id":115,"depth":251,"text":115},{"id":2229,"depth":251,"text":2230},{"id":2312,"depth":251,"text":2313,"children":3614},[3615,3616],{"id":2348,"depth":260,"text":2349},{"id":2515,"depth":260,"text":2516},{"id":2577,"depth":251,"text":2578,"children":3618},[3619,3620,3621],{"id":2584,"depth":260,"text":2584},{"id":2590,"depth":260,"text":2591},{"id":2597,"depth":260,"text":2597},{"id":2615,"depth":251,"text":2616,"children":3623},[3624,3625],{"id":2631,"depth":260,"text":2631},{"id":2646,"depth":260,"text":2646},{"id":2673,"depth":251,"text":2674,"children":3627},[3628,3629],{"id":2711,"depth":260,"text":2711},{"id":2726,"depth":260,"text":2726},{"id":2732,"depth":251,"text":2733,"children":3631},[3632,3633,3634],{"id":2742,"depth":260,"text":2743},{"id":2749,"depth":260,"text":2750},{"id":2756,"depth":260,"text":2757},{"id":2771,"depth":251,"text":2772,"children":3636},[3637,3638],{"id":2537,"depth":260,"text":2537},{"id":2792,"depth":260,"text":2792},{"id":2801,"depth":251,"text":2802,"children":3640},[3641],{"id":2849,"depth":260,"text":2850},{"id":2856,"depth":251,"text":2857},{"id":2881,"depth":251,"text":2882},{"id":2909,"depth":251,"text":2910},{"id":2951,"depth":251,"text":2952},{"id":3000,"depth":251,"text":3001},{"id":3035,"depth":251,"text":3036},{"id":3070,"depth":251,"text":3071},{"id":3250,"depth":251,"text":3251,"children":3650},[3651,3652,3653],{"id":3254,"depth":260,"text":3255},{"id":3264,"depth":260,"text":3265},{"id":3280,"depth":260,"text":3281},{"id":3290,"depth":251,"text":3291},{"id":3516,"depth":251,"text":3517,"children":3656},[3657,3658,3659,3660,3661,3662],{"id":3520,"depth":260,"text":3521},{"id":3527,"depth":260,"text":3528},{"id":3534,"depth":260,"text":3535},{"id":3541,"depth":260,"text":3542},{"id":3548,"depth":260,"text":3549},{"id":3555,"depth":260,"text":3556},{"id":3562,"depth":251,"text":3563},{"id":3596,"depth":251,"text":3596},"wiki:java:jdk8-synchronized-lock-states","从 Mark Word、Lock Record 与 ObjectMonitor 出发，系统理解 JDK 8 HotSpot 中偏向锁、轻量级锁、重量级锁的获取、撤销、膨胀和释放过程。","advanced",{},35,"\u002Fjava\u002Fjdk8-synchronized-lock-states","并发编程",{"title":2161,"description":3666},"java\u002Fjdk8-synchronized-lock-states","2026-07-23","pzK2g0rnXBsTfHk45mk4P4Dlca-bkRaWaNMeJy8yZRA",{"id":3677,"title":3604,"body":3678,"commentId":4582,"description":4583,"difficulty":4584,"draft":2148,"extension":2149,"meta":4585,"navigation":756,"order":4586,"path":4587,"section":2153,"seo":4588,"stem":4589,"updated":4590,"__hash__":4591},"java\u002Fjava\u002Fsynchronized-vs-reentrantlock.md",{"type":7,"value":3679,"toc":4560},[3680,3683,3686,3691,3694,3701,3703,3712,3714,3722,3732,3736,3738,3744,3747,3753,3756,3759,3765,3771,3774,3778,3930,3932,3936,3939,3944,3947,3953,3956,3959,3965,3968,3970,3974,3980,3983,3986,3992,3995,4000,4002,4006,4009,4012,4018,4021,4024,4030,4033,4039,4041,4047,4050,4052,4056,4059,4065,4068,4074,4079,4082,4084,4088,4093,4096,4102,4108,4114,4117,4123,4126,4132,4135,4141,4144,4150,4153,4159,4162,4164,4168,4171,4174,4180,4183,4189,4192,4195,4198,4204,4207,4210,4216,4219,4225,4232,4234,4238,4242,4248,4251,4254,4260,4267,4273,4276,4282,4285,4287,4291,4294,4297,4313,4316,4321,4324,4326,4330,4333,4353,4355,4361,4364,4369,4371,4375,4377,4403,4406,4412,4415,4417,4421,4466,4469,4474,4481,4487,4490,4492,4496,4500,4503,4507,4510,4514,4517,4525,4529,4532,4536,4539,4542,4548,4550,4553],[18,3681,3682],{},"synchronized和reentrantLock的区别？",[10,3684,3685],{"id":3685},"面试标准回答",[105,3687,3688],{},[18,3689,3690],{},"synchronized 是 JVM 层面的内置锁，语法简单，进入同步块后自动加锁，退出时自动释放，支持可重入和内存可见性。ReentrantLock 是基于 AQS 实现的显式锁，同样支持可重入，但提供了公平锁、可响应中断获取锁、tryLock、超时获取以及多个 Condition 等高级能力。两者在现代 JDK 中性能差距通常不是主要选型依据；功能简单时优先 synchronized，需要精细控制时使用 ReentrantLock，并且必须在 finally 中释放锁。",[18,3692,3693],{},"一句话记忆：",[105,3695,3696],{},[18,3697,3698],{},[110,3699,3700],{},"synchronized 简单自动，ReentrantLock 灵活可控。",[14,3702,115],{"id":115},[18,3704,3705],{},[119,3706,3709],{"href":3707,"target":122,"rel":3708},"\u002Fimages\u002Fwiki\u002Fjava\u002Fsynchronized-vs-reentrantlock.svg",[124,125],[127,3710],{"src":3707,"alt":3711},"synchronized 与 ReentrantLock 核心差异和选型建议",[10,3713,132],{"id":132},[18,3715,3716,40,3718,3721],{},[22,3717,2172],{},[22,3719,3720],{},"ReentrantLock"," 都能实现互斥和可见性，但定位不同：",[105,3723,3724],{},[18,3725,3726,3728,3729,3731],{},[22,3727,2172],{}," 是 JVM 原生关键字，语法简单；",[22,3730,3720],{}," 是 JUC 提供的显式锁，功能更丰富、控制能力更强。",[14,3733,3735],{"id":3734},"一基本用法","一、基本用法",[473,3737,2172],{"id":2172},[144,3739,3742],{"className":3740,"code":3741,"language":149},[147],"synchronized (lock) {\n    \u002F\u002F 临界区\n}\n",[22,3743,3741],{"__ignoreMap":150},[18,3745,3746],{},"或者：",[144,3748,3751],{"className":3749,"code":3750,"language":149},[147],"public synchronized void method() {\n}\n",[22,3752,3750],{"__ignoreMap":150},[18,3754,3755],{},"锁会自动释放。",[473,3757,3720],{"id":3758},"reentrantlock",[144,3760,3763],{"className":3761,"code":3762,"language":149},[147],"ReentrantLock lock = new ReentrantLock();\n\nlock.lock();\ntry {\n    \u002F\u002F 临界区\n} finally {\n    lock.unlock();\n}\n",[22,3764,3762],{"__ignoreMap":150},[18,3766,3767,3768,3770],{},"必须手动释放，所以通常必须写在 ",[22,3769,1279],{}," 中。",[3772,3773],"hr",{},[14,3775,3777],{"id":3776},"二核心区别","二、核心区别",[299,3779,3780,3790],{},[302,3781,3782],{},[305,3783,3784,3786,3788],{},[308,3785,950],{},[308,3787,2172],{},[308,3789,3720],{},[315,3791,3792,3803,3814,3824,3834,3847,3859,3871,3882,3897,3908,3919],{},[305,3793,3794,3797,3800],{},[320,3795,3796],{},"实现层次",[320,3798,3799],{},"JVM 关键字、Monitor",[320,3801,3802],{},"Java 类，基于 AQS",[305,3804,3805,3808,3811],{},[320,3806,3807],{},"加锁释放",[320,3809,3810],{},"自动",[320,3812,3813],{},"手动",[305,3815,3816,3819,3822],{},[320,3817,3818],{},"可重入",[320,3820,3821],{},"支持",[320,3823,3821],{},[305,3825,3826,3829,3832],{},[320,3827,3828],{},"公平锁",[320,3830,3831],{},"不支持显式配置",[320,3833,3821],{},[305,3835,3836,3839,3842],{},[320,3837,3838],{},"可中断获取锁",[320,3840,3841],{},"不支持",[320,3843,3844],{},[22,3845,3846],{},"lockInterruptibly()",[305,3848,3849,3852,3854],{},[320,3850,3851],{},"尝试获取锁",[320,3853,3841],{},[320,3855,3856],{},[22,3857,3858],{},"tryLock()",[305,3860,3861,3864,3866],{},[320,3862,3863],{},"超时获取锁",[320,3865,3841],{},[320,3867,3868],{},[22,3869,3870],{},"tryLock(timeout, unit)",[305,3872,3873,3876,3879],{},[320,3874,3875],{},"条件队列",[320,3877,3878],{},"一个 Monitor WaitSet",[320,3880,3881],{},"可创建多个 Condition",[305,3883,3884,3887,3892],{},[320,3885,3886],{},"等待通知",[320,3888,3889],{},[22,3890,3891],{},"wait\u002Fnotify\u002FnotifyAll",[320,3893,3894],{},[22,3895,3896],{},"await\u002Fsignal\u002FsignalAll",[305,3898,3899,3902,3905],{},[320,3900,3901],{},"锁状态查询",[320,3903,3904],{},"能力有限",[320,3906,3907],{},"提供较多查询方法",[305,3909,3910,3913,3916],{},[320,3911,3912],{},"编码复杂度",[320,3914,3915],{},"低",[320,3917,3918],{},"较高",[305,3920,3921,3924,3927],{},[320,3922,3923],{},"异常释放",[320,3925,3926],{},"自动释放",[320,3928,3929],{},"必须 finally unlock",[3772,3931],{},[10,3933,3935],{"id":3934},"三两者都支持可重入","三、两者都支持可重入",[18,3937,3938],{},"可重入指：",[105,3940,3941],{},[18,3942,3943],{},"同一个线程已经持有锁时，可以再次获取同一把锁。",[473,3945,2172],{"id":3946},"synchronized-1",[144,3948,3951],{"className":3949,"code":3950,"language":149},[147],"public synchronized void methodA() {\n    methodB();\n}\n\npublic synchronized void methodB() {\n}\n",[22,3952,3950],{"__ignoreMap":150},[18,3954,3955],{},"同一个对象上的两个同步方法，线程可以重入。",[473,3957,3720],{"id":3958},"reentrantlock-1",[144,3960,3963],{"className":3961,"code":3962,"language":149},[147],"lock.lock();\ntry {\n    lock.lock();\n    try {\n        \u002F\u002F 重入\n    } finally {\n        lock.unlock();\n    }\n} finally {\n    lock.unlock();\n}\n",[22,3964,3962],{"__ignoreMap":150},[18,3966,3967],{},"获取几次，就必须释放几次。",[3772,3969],{},[10,3971,3973],{"id":3972},"四reentrantlock-支持公平锁","四、ReentrantLock 支持公平锁",[144,3975,3978],{"className":3976,"code":3977,"language":149},[147],"ReentrantLock fairLock = new ReentrantLock(true);\n",[22,3979,3977],{"__ignoreMap":150},[18,3981,3982],{},"公平锁会尽量按 AQS 队列顺序获取锁。",[18,3984,3985],{},"默认是非公平锁：",[144,3987,3990],{"className":3988,"code":3989,"language":149},[147],"ReentrantLock lock = new ReentrantLock();\n",[22,3991,3989],{"__ignoreMap":150},[18,3993,3994],{},"非公平锁允许新线程插队，吞吐量通常更高。",[18,3996,3997,3999],{},[22,3998,2172],{}," 没有 API 让你指定公平性，通常按非公平竞争理解。",[3772,4001],{},[10,4003,4005],{"id":4004},"五reentrantlock-支持可响应中断等待","五、ReentrantLock 支持可响应中断等待",[18,4007,4008],{},"假设线程正在等待锁。",[18,4010,4011],{},"使用：",[144,4013,4016],{"className":4014,"code":4015,"language":149},[147],"lock.lock();\n",[22,4017,4015],{"__ignoreMap":150},[18,4019,4020],{},"即使线程被中断，也不会因为中断立即退出获取锁过程。",[18,4022,4023],{},"而：",[144,4025,4028],{"className":4026,"code":4027,"language":149},[147],"lock.lockInterruptibly();\n",[22,4029,4027],{"__ignoreMap":150},[18,4031,4032],{},"等待期间如果收到中断，会抛出：",[144,4034,4037],{"className":4035,"code":4036,"language":149},[147],"InterruptedException\n",[22,4038,4036],{"__ignoreMap":150},[18,4040,686],{},[144,4042,4045],{"className":4043,"code":4044,"language":149},[147],"try {\n    lock.lockInterruptibly();\n    try {\n        doWork();\n    } finally {\n        lock.unlock();\n    }\n} catch (InterruptedException e) {\n    Thread.currentThread().interrupt();\n}\n",[22,4046,4044],{"__ignoreMap":150},[18,4048,4049],{},"这对于避免线程长时间死等、响应取消请求很有价值。",[3772,4051],{},[10,4053,4055],{"id":4054},"六reentrantlock-支持尝试获取和超时","六、ReentrantLock 支持尝试获取和超时",[473,4057,4058],{"id":4058},"立即尝试",[144,4060,4063],{"className":4061,"code":4062,"language":149},[147],"if (lock.tryLock()) {\n    try {\n        doWork();\n    } finally {\n        lock.unlock();\n    }\n} else {\n    \u002F\u002F 获取失败，走降级逻辑\n}\n",[22,4064,4062],{"__ignoreMap":150},[473,4066,4067],{"id":4067},"超时尝试",[144,4069,4072],{"className":4070,"code":4071,"language":149},[147],"if (lock.tryLock(2, TimeUnit.SECONDS)) {\n    try {\n        doWork();\n    } finally {\n        lock.unlock();\n    }\n} else {\n    \u002F\u002F 两秒内未拿到锁\n}\n",[22,4073,4071],{"__ignoreMap":150},[18,4075,4076,4078],{},[22,4077,2172],{}," 一旦进入竞争，只能等待，无法直接设置超时。",[18,4080,4081],{},"这也是 ReentrantLock 在需要降级、超时控制时的重要优势。",[3772,4083],{},[10,4085,4087],{"id":4086},"七condition-比-waitnotify-更灵活","七、Condition 比 wait\u002Fnotify 更灵活",[18,4089,4090,4092],{},[22,4091,2172],{}," 的一个 Monitor 只有一个 WaitSet。",[18,4094,4095],{},"例如阻塞队列里：",[144,4097,4100],{"className":4098,"code":4099,"language":149},[147],"生产者等待 notFull\n消费者等待 notEmpty\n",[22,4101,4099],{"__ignoreMap":150},[18,4103,4104,4105,4107],{},"使用 ",[22,4106,2172],{}," 时，生产者和消费者都在同一个 WaitSet 中，通常要：",[144,4109,4112],{"className":4110,"code":4111,"language":149},[147],"notifyAll();\n",[22,4113,4111],{"__ignoreMap":150},[18,4115,4116],{},"而 ReentrantLock 可以创建多个 Condition：",[144,4118,4121],{"className":4119,"code":4120,"language":149},[147],"Condition notFull = lock.newCondition();\nCondition notEmpty = lock.newCondition();\n",[22,4122,4120],{"__ignoreMap":150},[18,4124,4125],{},"生产者等待：",[144,4127,4130],{"className":4128,"code":4129,"language":149},[147],"while (queue.size() == capacity) {\n    notFull.await();\n}\n",[22,4131,4129],{"__ignoreMap":150},[18,4133,4134],{},"消费者等待：",[144,4136,4139],{"className":4137,"code":4138,"language":149},[147],"while (queue.isEmpty()) {\n    notEmpty.await();\n}\n",[22,4140,4138],{"__ignoreMap":150},[18,4142,4143],{},"生产完成：",[144,4145,4148],{"className":4146,"code":4147,"language":149},[147],"notEmpty.signal();\n",[22,4149,4147],{"__ignoreMap":150},[18,4151,4152],{},"消费完成：",[144,4154,4157],{"className":4155,"code":4156,"language":149},[147],"notFull.signal();\n",[22,4158,4156],{"__ignoreMap":150},[18,4160,4161],{},"这样可以精确唤醒，减少无效唤醒。",[3772,4163],{},[10,4165,4167],{"id":4166},"八底层实现不同","八、底层实现不同",[14,4169,2172],{"id":4170},"synchronized-2",[18,4172,4173],{},"经典理解：",[144,4175,4178],{"className":4176,"code":4177,"language":149},[147],"对象头 Mark Word\n+\nMonitor\n+\nJVM 锁优化\n",[22,4179,4177],{"__ignoreMap":150},[18,4181,4182],{},"JDK 8 中可能经历：",[144,4184,4187],{"className":4185,"code":4186,"language":149},[147],"偏向锁\n→ 轻量级锁\n→ 重量级锁\n",[22,4188,4186],{"__ignoreMap":150},[18,4190,4191],{},"这是 HotSpot 的 JVM 实现优化。",[14,4193,3720],{"id":4194},"reentrantlock-2",[18,4196,4197],{},"主要基于：",[144,4199,4202],{"className":4200,"code":4201,"language":149},[147],"AbstractQueuedSynchronizer\n",[22,4203,4201],{"__ignoreMap":150},[18,4205,4206],{},"也就是 AQS。",[18,4208,4209],{},"核心状态：",[144,4211,4214],{"className":4212,"code":4213,"language":149},[147],"volatile int state;\n",[22,4215,4213],{"__ignoreMap":150},[18,4217,4218],{},"独占锁时：",[144,4220,4223],{"className":4221,"code":4222,"language":149},[147],"state = 0  未加锁\nstate = 1  第一次获取\nstate > 1  重入次数\n",[22,4224,4222],{"__ignoreMap":150},[18,4226,4227,4228,4231],{},"竞争失败的线程进入 CLH 变体同步队列，并通过 ",[22,4229,4230],{},"park\u002Funpark"," 等待和唤醒。",[3772,4233],{},[10,4235,4237],{"id":4236},"九异常情况下的差异","九、异常情况下的差异",[18,4239,4240,431],{},[22,4241,2172],{},[144,4243,4246],{"className":4244,"code":4245,"language":149},[147],"synchronized (lock) {\n    throw new RuntimeException();\n}\n",[22,4247,4245],{"__ignoreMap":150},[18,4249,4250],{},"离开同步块时，JVM 自动释放锁。",[18,4252,4253],{},"ReentrantLock：",[144,4255,4258],{"className":4256,"code":4257,"language":149},[147],"lock.lock();\ndoWork();\nlock.unlock();\n",[22,4259,4257],{"__ignoreMap":150},[18,4261,4262,4263,4266],{},"如果 ",[22,4264,4265],{},"doWork()"," 抛异常：",[144,4268,4271],{"className":4269,"code":4270,"language":149},[147],"unlock 没执行\n→ 锁永久未释放\n",[22,4272,4270],{"__ignoreMap":150},[18,4274,4275],{},"所以必须写：",[144,4277,4280],{"className":4278,"code":4279,"language":149},[147],"lock.lock();\ntry {\n    doWork();\n} finally {\n    lock.unlock();\n}\n",[22,4281,4279],{"__ignoreMap":150},[18,4283,4284],{},"这是 ReentrantLock 最常见的编码风险。",[3772,4286],{},[10,4288,4290],{"id":4289},"十性能区别","十、性能区别",[18,4292,4293],{},"早期 JDK 中，ReentrantLock 性能常常明显优于 synchronized。",[18,4295,4296],{},"但 JDK 6 以后，JVM 对 synchronized 做了大量优化：",[159,4298,4299,4302,4304,4307,4310],{},[162,4300,4301],{},"偏向锁",[162,4303,2446],{},[162,4305,4306],{},"自旋",[162,4308,4309],{},"锁消除",[162,4311,4312],{},"锁粗化",[18,4314,4315],{},"现代 JDK 中：",[105,4317,4318],{},[18,4319,4320],{},"两者性能差异通常不是选型的首要依据。",[18,4322,4323],{},"应该根据功能需求选择，而不是简单认为 ReentrantLock 一定更快。",[3772,4325],{},[10,4327,4329],{"id":4328},"十一什么时候使用-synchronized","十一、什么时候使用 synchronized",[18,4331,4332],{},"适合：",[159,4334,4335,4338,4341,4344,4347,4350],{},[162,4336,4337],{},"临界区简单",[162,4339,4340],{},"不需要公平锁",[162,4342,4343],{},"不需要超时获取",[162,4345,4346],{},"不需要中断锁等待",[162,4348,4349],{},"只有一个等待条件",[162,4351,4352],{},"希望代码简单、自动释放锁",[18,4354,686],{},[144,4356,4359],{"className":4357,"code":4358,"language":149},[147],"public synchronized void increment() {\n    count++;\n}\n",[22,4360,4358],{"__ignoreMap":150},[18,4362,4363],{},"一般优先原则：",[105,4365,4366],{},[18,4367,4368],{},"能用 synchronized 清晰表达时，优先用 synchronized。",[3772,4370],{},[10,4372,4374],{"id":4373},"十二什么时候使用-reentrantlock","十二、什么时候使用 ReentrantLock",[18,4376,4332],{},[159,4378,4379,4382,4388,4391,4394,4397,4400],{},[162,4380,4381],{},"需要公平锁",[162,4383,4384,4385],{},"需要 ",[22,4386,4387],{},"tryLock",[162,4389,4390],{},"需要超时获取锁",[162,4392,4393],{},"需要可中断等待",[162,4395,4396],{},"需要多个 Condition",[162,4398,4399],{},"需要查询等待队列、持锁状态",[162,4401,4402],{},"需要更精细的锁控制",[18,4404,4405],{},"例如转账时避免死锁：",[144,4407,4410],{"className":4408,"code":4409,"language":149},[147],"if (accountA.lock.tryLock(100, TimeUnit.MILLISECONDS)) {\n    try {\n        if (accountB.lock.tryLock(100, TimeUnit.MILLISECONDS)) {\n            try {\n                transfer();\n            } finally {\n                accountB.lock.unlock();\n            }\n        }\n    } finally {\n        accountA.lock.unlock();\n    }\n}\n",[22,4411,4409],{"__ignoreMap":150},[18,4413,4414],{},"使用 synchronized 很难实现这种超时退避。",[3772,4416],{},[10,4418,4420],{"id":4419},"十三waitnotify-和-awaitsignal-对应关系","十三、wait\u002Fnotify 和 await\u002Fsignal 对应关系",[299,4422,4423,4431],{},[302,4424,4425],{},[305,4426,4427,4429],{},[308,4428,2172],{},[308,4430,3720],{},[315,4432,4433,4444,4455],{},[305,4434,4435,4439],{},[320,4436,4437],{},[22,4438,2603],{},[320,4440,4441],{},[22,4442,4443],{},"Condition.await()",[305,4445,4446,4450],{},[320,4447,4448],{},[22,4449,3023],{},[320,4451,4452],{},[22,4453,4454],{},"Condition.signal()",[305,4456,4457,4461],{},[320,4458,4459],{},[22,4460,3026],{},[320,4462,4463],{},[22,4464,4465],{},"Condition.signalAll()",[18,4467,4468],{},"两者都要求：",[105,4470,4471],{},[18,4472,4473],{},"调用等待或通知方法前，必须先持有对应的锁。",[18,4475,4476,4477,4480],{},"并且等待都应该放在 ",[22,4478,4479],{},"while"," 中：",[144,4482,4485],{"className":4483,"code":4484,"language":149},[147],"while (!conditionSatisfied()) {\n    condition.await();\n}\n",[22,4486,4484],{"__ignoreMap":150},[18,4488,4489],{},"防止虚假唤醒和竞争后条件失效。",[3772,4491],{},[10,4493,4495],{"id":4494},"十四常见误区","十四、常见误区",[473,4497,4499],{"id":4498},"误区一reentrantlock-才支持可重入","误区一：ReentrantLock 才支持可重入",[18,4501,4502],{},"不对。两者都可重入。",[473,4504,4506],{"id":4505},"误区二reentrantlock-一定比-synchronized-快","误区二：ReentrantLock 一定比 synchronized 快",[18,4508,4509],{},"不对。现代 JVM 下要看场景，功能差异比纯性能更重要。",[473,4511,4513],{"id":4512},"误区三synchronized-会自动释放reentrantlock-不会","误区三：synchronized 会自动释放，ReentrantLock 不会",[18,4515,4516],{},"更准确地说：",[159,4518,4519,4522],{},[162,4520,4521],{},"synchronized 离开同步块时自动释放",[162,4523,4524],{},"ReentrantLock 必须显式 unlock",[473,4526,4528],{"id":4527},"误区四公平锁一定更好","误区四：公平锁一定更好",[18,4530,4531],{},"公平锁吞吐量通常更低，只在有明确公平性需求时使用。",[473,4533,4535],{"id":4534},"误区五trylock-失败后可以直接-unlock","误区五：tryLock 失败后可以直接 unlock",[18,4537,4538],{},"不可以。只有成功获取锁后才能释放。",[18,4540,4541],{},"正确写法：",[144,4543,4546],{"className":4544,"code":4545,"language":149},[147],"boolean locked = false;\ntry {\n    locked = lock.tryLock();\n    if (!locked) {\n        return;\n    }\n\n    doWork();\n} finally {\n    if (locked) {\n        lock.unlock();\n    }\n}\n",[22,4547,4545],{"__ignoreMap":150},[3772,4549],{},[14,4551,4552],{"id":4552},"相关主题",[159,4554,4555],{},[162,4556,4557],{},[119,4558,2161],{"href":4559},"\u002Fwiki\u002Fjava\u002Fjdk8-synchronized-lock-states\u002F",{"title":150,"searchDepth":251,"depth":251,"links":4561},[4562,4563,4567,4573,4574,4581],{"id":115,"depth":251,"text":115},{"id":3734,"depth":251,"text":3735,"children":4564},[4565,4566],{"id":2172,"depth":260,"text":2172},{"id":3758,"depth":260,"text":3720},{"id":3776,"depth":251,"text":3777,"children":4568},[4569,4570,4571,4572],{"id":3946,"depth":260,"text":2172},{"id":3958,"depth":260,"text":3720},{"id":4058,"depth":260,"text":4058},{"id":4067,"depth":260,"text":4067},{"id":4170,"depth":251,"text":2172},{"id":4194,"depth":251,"text":3720,"children":4575},[4576,4577,4578,4579,4580],{"id":4498,"depth":260,"text":4499},{"id":4505,"depth":260,"text":4506},{"id":4512,"depth":260,"text":4513},{"id":4527,"depth":260,"text":4528},{"id":4534,"depth":260,"text":4535},{"id":4552,"depth":251,"text":4552},"wiki:java:synchronized-vs-reentrantlock","对比 synchronized 与 ReentrantLock 的实现、可重入、公平锁、中断、超时、Condition 和适用场景。","intermediate",{},71,"\u002Fjava\u002Fsynchronized-vs-reentrantlock",{"title":3604,"description":4583},"java\u002Fsynchronized-vs-reentrantlock","2026-07-21","HvwBdM7dDk9OpIeOwtk34xX3bS9W_vivGkHtOntEom0",{"id":4593,"title":4594,"body":4595,"commentId":5889,"description":5890,"difficulty":3667,"draft":2148,"extension":2149,"meta":5891,"navigation":756,"order":5892,"path":5893,"section":2153,"seo":5894,"stem":5895,"updated":5896,"__hash__":5897},"java\u002Fjava\u002Fkafka-message-loss-prevention.md","Kafka 如何保证消息不丢失",{"type":7,"value":4596,"toc":5844},[4597,4600,4602,4619,4621,4628,4630,4639,4641,4644,4651,4654,4658,4661,4675,4678,4682,4688,4695,4750,4753,4767,4776,4780,4783,4826,4829,4832,4845,4849,4852,4868,4875,4929,4935,4939,4942,4948,4951,4954,4960,4963,4967,4971,4974,4980,4983,4986,4990,4993,4996,5002,5005,5011,5016,5022,5025,5031,5034,5039,5042,5046,5059,5065,5068,5077,5080,5086,5096,5101,5104,5108,5112,5124,5127,5133,5136,5140,5143,5149,5152,5262,5265,5269,5272,5278,5281,5298,5305,5309,5312,5318,5321,5334,5337,5340,5344,5347,5353,5357,5360,5363,5369,5372,5375,5395,5402,5405,5411,5417,5421,5424,5426,5432,5438,5441,5455,5459,5530,5533,5540,5544,5594,5597,5604,5607,5611,5721,5725,5732,5740,5744,5750,5754,5757,5761,5764,5768,5771,5775,5778,5798,5801,5804,5841],[10,4598,4594],{"id":4599},"kafka-如何保证消息不丢失",[14,4601,16],{"id":16},[105,4603,4604],{},[18,4605,4606,4607,4610,4611,4614,4615,4618],{},"Kafka 保证消息不丢失需要覆盖生产者、Broker 和消费者三段链路。生产者侧使用 ",[22,4608,4609],{},"acks=all","、开启重试和幂等生产，并检查发送回调，对最终失败进行落库、告警或补偿；Broker 侧通常设置三副本、",[22,4612,4613],{},"min.insync.replicas=2","，并关闭非同步副本选举，即配置 ",[22,4616,4617],{},"unclean.leader.election.enable=false","，保证消息在足够多的同步副本写入后才确认成功；消费者侧关闭自动提交，在业务处理成功后再提交 Offset，失败时不提交以便重新消费。这样通常实现至少一次投递，所以消费端还必须通过事件 ID、唯一索引或状态机保证幂等。涉及数据库与 Kafka 的一致性时，可以使用 Transactional Outbox；消费端需要应对重复消息时，可以使用 Inbox。Kafka 的消息不丢失是端到端保障，不是只配置一个参数。",[18,4620,103],{},[105,4622,4623],{},[18,4624,4625],{},[110,4626,4627],{},"生产者确认成功，Broker 多副本可靠保存，消费者处理成功后提交，业务端用幂等和补偿兜底。",[14,4629,115],{"id":115},[18,4631,4632],{},[119,4633,4636],{"href":4634,"target":122,"rel":4635},"\u002Fimages\u002Fwiki\u002Fjava\u002Fkafka-message-loss-prevention.svg",[124,125],[127,4637],{"src":4634,"alt":4638},"Kafka 消息不丢失的端到端保障链路",[14,4640,132],{"id":132},[18,4642,4643],{},"Kafka 消息不丢失不是靠某一个参数实现的，而是一个端到端问题：",[105,4645,4646],{},[18,4647,4648],{},[110,4649,4650],{},"生产者必须确认消息发送成功，Broker 必须把消息可靠地保存在足够多的副本中，消费者必须在业务处理成功后再提交 Offset。",[18,4652,4653],{},"其中任何一个环节处理不当，都可能出现“业务上看起来消息丢了”的情况。",[14,4655,4657],{"id":4656},"一先明确什么叫消息丢失","一、先明确什么叫“消息丢失”",[18,4659,4660],{},"常见的丢失场景主要有四类：",[2695,4662,4663,4666,4669,4672],{},[162,4664,4665],{},"生产者调用发送方法后没有检查结果，实际发送失败却被业务忽略。",[162,4667,4668],{},"Leader 收到消息后就返回成功，消息还未复制到 Follower，Leader 随后故障。",[162,4670,4671],{},"消费者先提交 Offset，再执行业务；业务处理失败后，消息不会再次投递。",[162,4673,4674],{},"Kafka 中的消息没有丢，但写数据库、调用下游等业务操作失败，又没有补偿机制。",[18,4676,4677],{},"因此，Kafka 中仍能查到消息，不代表业务一定处理成功；反过来，业务没有结果，也不一定是 Broker 丢了消息。",[14,4679,4681],{"id":4680},"二生产者如何避免丢消息","二、生产者如何避免丢消息",[473,4683,4685,4686],{"id":4684},"_1-使用-acksall","1. 使用 ",[22,4687,4609],{},[18,4689,4690,4691,4694],{},"生产者的 ",[22,4692,4693],{},"acks"," 决定 Broker 在什么条件下确认发送成功：",[299,4696,4697,4710],{},[302,4698,4699],{},[305,4700,4701,4704,4707],{},[308,4702,4703],{},"配置",[308,4705,4706],{},"确认时机",[308,4708,4709],{},"风险",[315,4711,4712,4725,4738],{},[305,4713,4714,4719,4722],{},[320,4715,4716],{},[22,4717,4718],{},"acks=0",[320,4720,4721],{},"不等待 Broker 响应",[320,4723,4724],{},"发送失败也无法感知，风险最高",[305,4726,4727,4732,4735],{},[320,4728,4729],{},[22,4730,4731],{},"acks=1",[320,4733,4734],{},"Leader 写入本地日志后返回",[320,4736,4737],{},"Leader 故障且副本尚未同步时可能丢失",[305,4739,4740,4744,4747],{},[320,4741,4742],{},[22,4743,4609],{},[320,4745,4746],{},"当前 ISR 中的副本都确认后返回",[320,4748,4749],{},"Kafka 能提供的最强生产者确认保证",[18,4751,4752],{},"关键配置：",[144,4754,4758],{"className":4755,"code":4756,"language":4757,"meta":150,"style":150},"language-properties shiki shiki-themes github-light github-dark","acks=all\n","properties",[22,4759,4760],{"__ignoreMap":150},[203,4761,4762,4764],{"class":205,"line":206},[203,4763,4693],{"class":209},[203,4765,4766],{"class":219},"=all\n",[18,4768,4769,4771,4772,4775],{},[22,4770,4609],{}," 并不等于绝对不丢。它还需要与副本数和 ",[22,4773,4774],{},"min.insync.replicas"," 配合，否则 ISR 中只剩 Leader 一个副本时，依然可能成功写入。",[473,4777,4779],{"id":4778},"_2-开启重试和幂等生产","2. 开启重试和幂等生产",[18,4781,4782],{},"网络抖动、Leader 切换等临时故障可能导致发送失败，生产者需要允许重试：",[144,4784,4786],{"className":4755,"code":4785,"language":4757,"meta":150,"style":150},"enable.idempotence=true\nacks=all\nretries=2147483647\nmax.in.flight.requests.per.connection=5\ndelivery.timeout.ms=120000\n",[22,4787,4788,4796,4802,4810,4818],{"__ignoreMap":150},[203,4789,4790,4793],{"class":205,"line":206},[203,4791,4792],{"class":209},"enable.idempotence",[203,4794,4795],{"class":219},"=true\n",[203,4797,4798,4800],{"class":205,"line":251},[203,4799,4693],{"class":209},[203,4801,4766],{"class":219},[203,4803,4804,4807],{"class":205,"line":260},[203,4805,4806],{"class":209},"retries",[203,4808,4809],{"class":219},"=2147483647\n",[203,4811,4812,4815],{"class":205,"line":278},[203,4813,4814],{"class":209},"max.in.flight.requests.per.connection",[203,4816,4817],{"class":219},"=5\n",[203,4819,4820,4823],{"class":205,"line":285},[203,4821,4822],{"class":209},"delivery.timeout.ms",[203,4824,4825],{"class":219},"=120000\n",[18,4827,4828],{},"幂等生产者会给消息附加 Producer ID 和序列号，使 Broker 能识别同一生产会话内的重复写入，避免因重试产生重复消息。",[18,4830,4831],{},"需要注意：",[159,4833,4834,4837,4842],{},[162,4835,4836],{},"幂等生产解决的是重试导致的重复写入，不是发送失败后的业务补偿。",[162,4838,4839,4841],{},[22,4840,4822],{}," 到期后，生产者仍可能最终失败。",[162,4843,4844],{},"业务不能无限依赖客户端重试，最终失败必须记录、告警或进入补偿流程。",[473,4846,4848],{"id":4847},"_3-必须检查发送结果","3. 必须检查发送结果",[18,4850,4851],{},"下面这种“只发送、不处理结果”的写法存在风险：",[144,4853,4855],{"className":197,"code":4854,"language":199,"meta":150,"style":150},"producer.send(record);\n",[22,4856,4857],{"__ignoreMap":150},[203,4858,4859,4862,4865],{"class":205,"line":206},[203,4860,4861],{"class":219},"producer.",[203,4863,4864],{"class":215},"send",[203,4866,4867],{"class":219},"(record);\n",[18,4869,4870,4871,4874],{},"应该检查回调或 ",[22,4872,4873],{},"Future"," 的执行结果：",[144,4876,4878],{"className":197,"code":4877,"language":199,"meta":150,"style":150},"producer.send(record, (metadata, exception) -> {\n    if (exception != null) {\n        \u002F\u002F 记录原始消息、告警并进入补偿流程\n        handleSendFailure(record, exception);\n    }\n});\n",[22,4879,4880,4894,4907,4912,4920,4924],{"__ignoreMap":150},[203,4881,4882,4884,4886,4889,4892],{"class":205,"line":206},[203,4883,4861],{"class":219},[203,4885,4864],{"class":215},[203,4887,4888],{"class":219},"(record, (metadata, exception) ",[203,4890,4891],{"class":209},"->",[203,4893,248],{"class":219},[203,4895,4896,4898,4901,4903,4905],{"class":205,"line":251},[203,4897,709],{"class":209},[203,4899,4900],{"class":219}," (exception ",[203,4902,1350],{"class":209},[203,4904,719],{"class":718},[203,4906,275],{"class":219},[203,4908,4909],{"class":205,"line":260},[203,4910,4911],{"class":281},"        \u002F\u002F 记录原始消息、告警并进入补偿流程\n",[203,4913,4914,4917],{"class":205,"line":278},[203,4915,4916],{"class":215},"        handleSendFailure",[203,4918,4919],{"class":219},"(record, exception);\n",[203,4921,4922],{"class":205,"line":285},[203,4923,751],{"class":219},[203,4925,4926],{"class":205,"line":541},[203,4927,4928],{"class":219},"});\n",[18,4930,4931,4932,4934],{},"序列化失败、鉴权失败、消息过大和超时等错误，最终都需要业务明确处理。不能把“调用过 ",[22,4933,4864],{},"”当成“消息已经可靠进入 Kafka”。",[473,4936,4938],{"id":4937},"_4-数据库与-kafka-的一致性","4. 数据库与 Kafka 的一致性",[18,4940,4941],{},"如果业务流程是：",[144,4943,4946],{"className":4944,"code":4945,"language":149,"meta":150},[147],"更新数据库\n→\n发送 Kafka 消息\n",[22,4947,4945],{"__ignoreMap":150},[18,4949,4950],{},"数据库更新成功、消息发送失败时，仍然会造成业务事件丢失。",[18,4952,4953],{},"常见解决方式是本地消息表，也叫 Transactional Outbox：",[144,4955,4958],{"className":4956,"code":4957,"language":149,"meta":150},[147],"同一个数据库事务\n├── 更新业务数据\n└── 写入待发送事件表\n\n事务提交后\n→ 后台任务投递 Kafka\n→ 成功后标记事件已发送\n",[22,4959,4957],{"__ignoreMap":150},[18,4961,4962],{},"这样即使 Kafka 暂时不可用，消息也可以从本地消息表继续重试。Kafka 事务可以保证 Kafka 内部多条记录及消费 Offset 的原子性，但不会自动把外部数据库事务包含进来。",[14,4964,4966],{"id":4965},"三broker-如何保证消息可靠保存","三、Broker 如何保证消息可靠保存",[473,4968,4970],{"id":4969},"_1-合理设置副本数","1. 合理设置副本数",[18,4972,4973],{},"生产环境通常使用：",[144,4975,4978],{"className":4976,"code":4977,"language":149,"meta":150},[147],"replication.factor = 3\n",[22,4979,4977],{"__ignoreMap":150},[18,4981,4982],{},"一个分区包含一个 Leader 和多个 Follower。生产者与 Leader 交互，Follower 从 Leader 复制日志。某个 Broker 故障后，可以从仍然同步的副本中选举新 Leader。",[18,4984,4985],{},"副本数提高的是容错能力，但副本只有真正保持同步才有意义。",[473,4987,4989],{"id":4988},"_2-理解-isr","2. 理解 ISR",[18,4991,4992],{},"ISR 是 In-Sync Replicas，即当前与 Leader 保持同步的副本集合。",[18,4994,4995],{},"例如一个三副本分区：",[144,4997,5000],{"className":4998,"code":4999,"language":149,"meta":150},[147],"Leader A\nFollower B\nFollower C\n\nISR = [A, B, C]\n",[22,5001,4999],{"__ignoreMap":150},[18,5003,5004],{},"如果 C 长时间跟不上 Leader，它会被移出 ISR：",[144,5006,5009],{"className":5007,"code":5008,"language":149,"meta":150},[147],"ISR = [A, B]\n",[22,5010,5008],{"__ignoreMap":150},[18,5012,5013,5015],{},[22,5014,4609],{}," 等待的是当前 ISR 的确认，而不是永远等待所有配置副本。",[473,5017,5019,5020],{"id":5018},"_3-配置-mininsyncreplicas","3. 配置 ",[22,5021,4774],{},[18,5023,5024],{},"推荐组合：",[144,5026,5029],{"className":5027,"code":5028,"language":149,"meta":150},[147],"replication.factor = 3\nmin.insync.replicas = 2\nacks = all\n",[22,5030,5028],{"__ignoreMap":150},[18,5032,5033],{},"它表达的含义是：",[105,5035,5036],{},[18,5037,5038],{},"至少要有两个同步副本可用，写入才允许成功。",[18,5040,5041],{},"如果 ISR 只剩一个副本，Broker 会拒绝写入。此时系统牺牲部分可用性，避免在单副本状态下继续写入并承担更高的数据丢失风险。",[473,5043,5045],{"id":5044},"_4-关闭非同步副本选举","4. 关闭非同步副本选举",[144,5047,5049],{"className":4755,"code":5048,"language":4757,"meta":150,"style":150},"unclean.leader.election.enable=false\n",[22,5050,5051],{"__ignoreMap":150},[203,5052,5053,5056],{"class":205,"line":206},[203,5054,5055],{"class":209},"unclean.leader.election.enable",[203,5057,5058],{"class":219},"=false\n",[18,5060,5061,5064],{},[22,5062,5063],{},"unclean leader election"," 指允许不在 ISR 中的副本被选举为 Leader。如果所有同步副本都不可用，非 ISR 副本可能缺少最新消息；将它选为 Leader 虽然能更快恢复分区服务，却可能造成日志回退和数据丢失。",[18,5066,5067],{},"因此，对数据可靠性要求较高的场景应明确配置：",[105,5069,5070],{},[18,5071,5072],{},[110,5073,5074,5075,33],{},"关闭非同步副本选举：",[22,5076,4617],{},[18,5078,5079],{},"这是一个典型的取舍：",[144,5081,5084],{"className":5082,"code":5083,"language":149,"meta":150},[147],"开启非同步副本选举：可用性更高，但可能丢失消息\n关闭非同步副本选举：数据可靠性更强，但分区可能暂时不可用\n",[22,5085,5083],{"__ignoreMap":150},[473,5087,5089,5090,5092,5093],{"id":5088},"_5-acksall-不等于每条消息都立即-fsync","5. ",[22,5091,4609],{}," 不等于每条消息都立即 ",[22,5094,5095],{},"fsync",[18,5097,5098,5099,33],{},"Kafka 的持久性主要依赖顺序写日志、操作系统页缓存和多副本机制。Broker 返回成功，并不表示每个副本都对该消息单独执行了一次物理磁盘 ",[22,5100,5095],{},[18,5102,5103],{},"实际生产中，通常通过跨 Broker 副本降低单机和单盘故障风险，而不是强制每条消息同步刷盘。若多个副本同时发生不可恢复故障，仍不存在数学意义上的绝对零丢失。",[14,5105,5107],{"id":5106},"四消费者如何避免消费丢失","四、消费者如何避免“消费丢失”",[473,5109,5111],{"id":5110},"_1-关闭自动提交-offset","1. 关闭自动提交 Offset",[144,5113,5115],{"className":4755,"code":5114,"language":4757,"meta":150,"style":150},"enable.auto.commit=false\n",[22,5116,5117],{"__ignoreMap":150},[203,5118,5119,5122],{"class":205,"line":206},[203,5120,5121],{"class":209},"enable.auto.commit",[203,5123,5058],{"class":219},[18,5125,5126],{},"自动提交可能出现以下顺序：",[144,5128,5131],{"className":5129,"code":5130,"language":149,"meta":150},[147],"拉取消息\n→ 自动提交 Offset\n→ 执行业务\n→ 进程崩溃\n",[22,5132,5130],{"__ignoreMap":150},[18,5134,5135],{},"重启后，消费者会从已提交的下一个 Offset 继续消费，刚才尚未处理成功的消息就被跳过了。",[473,5137,5139],{"id":5138},"_2-业务成功后再提交-offset","2. 业务成功后再提交 Offset",[18,5141,5142],{},"更可靠的顺序是：",[144,5144,5147],{"className":5145,"code":5146,"language":149,"meta":150},[147],"拉取消息\n→ 执行业务\n→ 业务成功\n→ 提交 Offset\n",[22,5148,5146],{"__ignoreMap":150},[18,5150,5151],{},"如果业务处理失败，就不提交 Offset，让消息后续重新消费：",[144,5153,5155],{"className":197,"code":5154,"language":199,"meta":150,"style":150},"while (true) {\n    ConsumerRecords\u003CString, String> records = consumer.poll(Duration.ofSeconds(1));\n\n    for (ConsumerRecord\u003CString, String> record : records) {\n        process(record);\n    }\n\n    consumer.commitSync();\n}\n",[22,5156,5157,5169,5206,5210,5233,5240,5244,5248,5258],{"__ignoreMap":150},[203,5158,5159,5161,5164,5167],{"class":205,"line":206},[203,5160,4479],{"class":209},[203,5162,5163],{"class":219}," (",[203,5165,5166],{"class":718},"true",[203,5168,275],{"class":219},[203,5170,5171,5174,5177,5180,5182,5185,5187,5190,5193,5196,5199,5201,5203],{"class":205,"line":251},[203,5172,5173],{"class":219},"    ConsumerRecords\u003C",[203,5175,5176],{"class":209},"String",[203,5178,5179],{"class":219},", ",[203,5181,5176],{"class":209},[203,5183,5184],{"class":219},"> records ",[203,5186,495],{"class":209},[203,5188,5189],{"class":219}," consumer.",[203,5191,5192],{"class":215},"poll",[203,5194,5195],{"class":219},"(Duration.",[203,5197,5198],{"class":215},"ofSeconds",[203,5200,220],{"class":219},[203,5202,2431],{"class":718},[203,5204,5205],{"class":219},"));\n",[203,5207,5208],{"class":205,"line":260},[203,5209,757],{"emptyLinePlaceholder":756},[203,5211,5212,5215,5218,5220,5222,5224,5227,5230],{"class":205,"line":278},[203,5213,5214],{"class":209},"    for",[203,5216,5217],{"class":219}," (ConsumerRecord\u003C",[203,5219,5176],{"class":209},[203,5221,5179],{"class":219},[203,5223,5176],{"class":209},[203,5225,5226],{"class":219},"> record ",[203,5228,5229],{"class":209},":",[203,5231,5232],{"class":219}," records) {\n",[203,5234,5235,5238],{"class":205,"line":285},[203,5236,5237],{"class":215},"        process",[203,5239,4867],{"class":219},[203,5241,5242],{"class":205,"line":541},[203,5243,751],{"class":219},[203,5245,5246],{"class":205,"line":549},[203,5247,757],{"emptyLinePlaceholder":756},[203,5249,5250,5253,5256],{"class":205,"line":787},[203,5251,5252],{"class":219},"    consumer.",[203,5254,5255],{"class":215},"commitSync",[203,5257,257],{"class":219},[203,5259,5260],{"class":205,"line":799},[203,5261,288],{"class":219},[18,5263,5264],{},"示例表达的是基本原则。实际批量消费时，如果一批消息中只有部分成功，需要准确管理各分区可提交的 Offset，不能直接把失败消息之后的位置一起提交。",[473,5266,5268],{"id":5267},"_3-为什么还需要业务幂等","3. 为什么还需要业务幂等",[18,5270,5271],{},"“处理成功后提交”可以避免消息被跳过，但会引入重复：",[144,5273,5276],{"className":5274,"code":5275,"language":149,"meta":150},[147],"业务处理成功\n→ 提交 Offset 前进程崩溃\n→ 重启后再次消费同一条消息\n",[22,5277,5275],{"__ignoreMap":150},[18,5279,5280],{},"因此，可靠消费通常采用至少一次投递，并在业务层保证幂等。常见方式包括：",[159,5282,5283,5286,5289,5292,5295],{},[162,5284,5285],{},"使用事件 ID 建立数据库唯一索引。",[162,5287,5288],{},"建立消费去重表或 Inbox 表。",[162,5290,5291],{},"使用业务状态机，只允许合法状态转换。",[162,5293,5294],{},"更新时带版本号或业务条件。",[162,5296,5297],{},"对扣款、发券等操作使用唯一业务流水号。",[105,5299,5300],{},[18,5301,5302],{},[110,5303,5304],{},"不丢消息通常意味着允许重复，再通过幂等消除重复影响。",[473,5306,5308],{"id":5307},"_4-kafka-内部链路的-exactly-once","4. Kafka 内部链路的 Exactly Once",[18,5310,5311],{},"如果消费 Kafka 后仍然写回 Kafka，可以使用 Kafka 事务，把输出记录和消费 Offset 放进同一个事务：",[144,5313,5316],{"className":5314,"code":5315,"language":149,"meta":150},[147],"消费消息\n→ 处理\n→ 写入下游 Topic\n→ sendOffsetsToTransaction\n→ 提交事务\n",[22,5317,5315],{"__ignoreMap":150},[18,5319,5320],{},"下游消费者设置：",[144,5322,5324],{"className":4755,"code":5323,"language":4757,"meta":150,"style":150},"isolation.level=read_committed\n",[22,5325,5326],{"__ignoreMap":150},[203,5327,5328,5331],{"class":205,"line":206},[203,5329,5330],{"class":209},"isolation.level",[203,5332,5333],{"class":219},"=read_committed\n",[18,5335,5336],{},"这样只读取已提交事务的数据。",[18,5338,5339],{},"但如果最终写入的是 MySQL 等外部系统，Kafka 事务并不能自动保证 Kafka 与数据库的原子性。生产方可以用 Outbox 保证事件可靠发出，消费方可以用 Inbox 应对重复消费，具体区别见下一节。",[14,5341,5343],{"id":5342},"五outbox-与-inbox-分别是什么","五、Outbox 与 Inbox 分别是什么",[18,5345,5346],{},"Outbox 和 Inbox 都借助数据库本地事务解决消息链路中的一致性问题，但它们位于不同位置：",[144,5348,5351],{"className":5349,"code":5350,"language":149,"meta":150},[147],"业务生产方 → Outbox → Kafka → Inbox → 业务消费方\n",[22,5352,5350],{"__ignoreMap":150},[473,5354,5356],{"id":5355},"_1-outbox保证业务事件可靠发出","1. Outbox：保证业务事件可靠发出",[18,5358,5359],{},"Outbox 位于消息生产方，用来解决“数据库更新成功，但 Kafka 消息发送失败”的双写不一致问题。",[18,5361,5362],{},"核心流程：",[144,5364,5367],{"className":5365,"code":5366,"language":149,"meta":150},[147],"同一个数据库事务\n├── 更新业务数据\n└── 写入 Outbox 事件表\n\n事务提交\n→ 后台投递程序扫描 Outbox\n→ 发送 Kafka\n→ 收到成功确认后标记已发送\n",[22,5368,5366],{"__ignoreMap":150},[18,5370,5371],{},"例如创建订单时，不直接把“订单创建事件”的可靠性寄托在一次 Kafka 调用上，而是在创建订单的同一个数据库事务中写入一条 Outbox 记录。即使 Kafka 暂时不可用，后台任务仍可继续重试。",[18,5373,5374],{},"Outbox 的关键点：",[159,5376,5377,5380,5386,5389,5392],{},[162,5378,5379],{},"业务数据和事件记录必须写入同一个数据库事务。",[162,5381,5382,5383,33],{},"每条事件应有全局唯一的 ",[22,5384,5385],{},"eventId",[162,5387,5388],{},"投递程序可以轮询事件表，也可以通过 CDC 捕获新增事件。",[162,5390,5391],{},"“发送成功但标记失败”可能导致再次投递，因此消费者仍需幂等。",[162,5393,5394],{},"Outbox 解决的是事件可靠发出，不保证消费者一定处理成功。",[18,5396,5397,5398,5401],{},"这里的 CDC 是 ",[110,5399,5400],{},"Change Data Capture（变更数据捕获）","。它会读取数据库的变更日志，例如 MySQL Binlog，捕获数据的新增、修改和删除，再把变更发送到 Kafka。常用工具有 Debezium、Canal。",[18,5403,5404],{},"在 Outbox 场景中，它的工作链路是：",[144,5406,5409],{"className":5407,"code":5408,"language":149,"meta":150},[147],"业务事务写入业务表和 Outbox 表\n→ CDC 监听 Binlog\n→ 捕获新增的 Outbox 记录\n→ 发送到 Kafka\n",[22,5410,5408],{"__ignoreMap":150},[18,5412,5413,5414,5416],{},"相比定时扫描 Outbox 表，CDC 延迟更低，也能减少频繁查询数据库的压力。但消息链路仍可能产生重复，因此必须保留 ",[22,5415,5385],{},"，并由消费端保证幂等。",[473,5418,5420],{"id":5419},"_2-inbox保证重复消息只产生一次业务效果","2. Inbox：保证重复消息只产生一次业务效果",[18,5422,5423],{},"Inbox 位于消息消费方，用来记录已经处理过的事件，主要解决至少一次投递带来的重复消费问题。",[18,5425,5362],{},[144,5427,5430],{"className":5428,"code":5429,"language":149,"meta":150},[147],"收到 Kafka 消息\n→ 开启数据库事务\n→ 根据 eventId 写入 Inbox 记录\n→ 执行业务更新\n→ 提交数据库事务\n→ 提交 Kafka Offset\n",[22,5431,5429],{"__ignoreMap":150},[18,5433,5434,5435,5437],{},"Inbox 表通常对 ",[22,5436,5385],{}," 建立唯一索引。重复消息到达时，如果发现该事件已经存在，就不再重复执行业务逻辑。",[18,5439,5440],{},"Inbox 的关键点：",[159,5442,5443,5446,5449,5452],{},[162,5444,5445],{},"Inbox 记录和业务更新必须处于同一个数据库事务。",[162,5447,5448],{},"唯一索引负责防止同一事件被并发重复处理。",[162,5450,5451],{},"数据库事务提交后、Offset 提交前发生故障，消息仍会重投，但 Inbox 可以识别重复。",[162,5453,5454],{},"如果消费逻辑还会调用外部 HTTP 接口，Inbox 无法自动让数据库和外部接口形成原子事务；仍需下游幂等、重试或补偿。",[473,5456,5458],{"id":5457},"_3-两者的区别","3. 两者的区别",[299,5460,5461,5473],{},[302,5462,5463],{},[305,5464,5465,5467,5470],{},[308,5466,950],{},[308,5468,5469],{},"Outbox",[308,5471,5472],{},"Inbox",[315,5474,5475,5486,5497,5508,5519],{},[305,5476,5477,5480,5483],{},[320,5478,5479],{},"所在位置",[320,5481,5482],{},"消息生产方",[320,5484,5485],{},"消息消费方",[305,5487,5488,5491,5494],{},[320,5489,5490],{},"主要目标",[320,5492,5493],{},"防止业务成功但事件未发出",[320,5495,5496],{},"防止重复消息重复产生业务效果",[305,5498,5499,5502,5505],{},[320,5500,5501],{},"本地事务内容",[320,5503,5504],{},"业务更新 + 写事件表",[320,5506,5507],{},"写消费记录 + 业务更新",[305,5509,5510,5513,5516],{},[320,5511,5512],{},"后台动作",[320,5514,5515],{},"把待发送事件投递到 Kafka",[320,5517,5518],{},"通常不需要再次投递",[305,5520,5521,5524,5527],{},[320,5522,5523],{},"仍需注意",[320,5525,5526],{},"投递过程可能产生重复消息",[320,5528,5529],{},"外部调用仍需幂等或补偿",[18,5531,5532],{},"一句话区分：",[105,5534,5535],{},[18,5536,5537],{},[110,5538,5539],{},"Outbox 保证“该发的消息最终发出去”，Inbox 保证“重复收到的消息不会重复生效”。",[14,5541,5543],{"id":5542},"六三种投递语义","六、三种投递语义",[299,5545,5546,5559],{},[302,5547,5548],{},[305,5549,5550,5553,5556],{},[308,5551,5552],{},"语义",[308,5554,5555],{},"Offset 与业务处理顺序",[308,5557,5558],{},"结果",[315,5560,5561,5572,5583],{},[305,5562,5563,5566,5569],{},[320,5564,5565],{},"At Most Once",[320,5567,5568],{},"先提交，再处理",[320,5570,5571],{},"可能丢失，通常不重复",[305,5573,5574,5577,5580],{},[320,5575,5576],{},"At Least Once",[320,5578,5579],{},"先处理，再提交",[320,5581,5582],{},"不轻易丢失，可能重复",[305,5584,5585,5588,5591],{},[320,5586,5587],{},"Exactly Once",[320,5589,5590],{},"事务或幂等机制协调",[320,5592,5593],{},"业务效果恰好一次",[18,5595,5596],{},"绝大多数业务系统采用：",[105,5598,5599],{},[18,5600,5601],{},[110,5602,5603],{},"Kafka 至少一次投递 + 消费端业务幂等。",[18,5605,5606],{},"这通常比追求所有环节的强事务更容易实现，也更便于扩展。",[14,5608,5610],{"id":5609},"七常见故障与保障措施","七、常见故障与保障措施",[299,5612,5613,5626],{},[302,5614,5615],{},[305,5616,5617,5620,5623],{},[308,5618,5619],{},"故障场景",[308,5621,5622],{},"可能后果",[308,5624,5625],{},"主要保障",[315,5627,5628,5639,5652,5665,5677,5688,5699,5710],{},[305,5629,5630,5633,5636],{},[320,5631,5632],{},"生产者网络抖动",[320,5634,5635],{},"发送失败或结果未知",[320,5637,5638],{},"重试、幂等生产、回调处理",[305,5640,5641,5644,5647],{},[320,5642,5643],{},"Leader 写入后立即故障",[320,5645,5646],{},"未同步消息丢失",[320,5648,5649,5651],{},[22,5650,4609],{},"、副本、ISR",[305,5653,5654,5657,5660],{},[320,5655,5656],{},"ISR 只剩一个副本",[320,5658,5659],{},"单点故障风险升高",[320,5661,5662,5664],{},[22,5663,4774],{}," 拒绝写入",[305,5666,5667,5670,5673],{},[320,5668,5669],{},"同步副本全部不可用",[320,5671,5672],{},"非同步副本选主后日志回退",[320,5674,5074,5675],{},[22,5676,4617],{},[305,5678,5679,5682,5685],{},[320,5680,5681],{},"消费者先提交后处理",[320,5683,5684],{},"业务消息被跳过",[320,5686,5687],{},"关闭自动提交，成功后提交",[305,5689,5690,5693,5696],{},[320,5691,5692],{},"处理成功但提交前崩溃",[320,5694,5695],{},"重复消费",[320,5697,5698],{},"业务幂等、唯一流水号",[305,5700,5701,5704,5707],{},[320,5702,5703],{},"数据库成功、Kafka 失败",[320,5705,5706],{},"业务事件未发出",[320,5708,5709],{},"Transactional Outbox",[305,5711,5712,5715,5718],{},[320,5713,5714],{},"Kafka 成功、下游失败",[320,5716,5717],{},"业务结果缺失",[320,5719,5720],{},"重试、死信、补偿、告警",[14,5722,5724],{"id":5723},"八常见误区","八、常见误区",[473,5726,5728,5729,5731],{"id":5727},"误区一配置-acksall-就绝对不会丢","误区一：配置 ",[22,5730,4609],{}," 就绝对不会丢",[18,5733,5734,5736,5737,5739],{},[22,5735,4609],{}," 只解决生产者到 Broker 的确认强度，还要配合副本数、",[22,5738,4774],{},"、正确选主和消费者提交策略。",[473,5741,5743],{"id":5742},"误区二配置无限重试就不会丢","误区二：配置无限重试就不会丢",[18,5745,5746,5747,5749],{},"重试仍然受 ",[22,5748,4822],{}," 等条件限制，而且权限错误、序列化错误等问题无法靠盲目重试解决。最终失败必须被业务感知和补偿。",[473,5751,5753],{"id":5752},"误区三手动提交-offset-就是-exactly-once","误区三：手动提交 Offset 就是 Exactly Once",[18,5755,5756],{},"手动提交只能帮助建立“处理成功后提交”的顺序，崩溃窗口仍可能造成重复消费，因此还需要业务幂等。",[473,5758,5760],{"id":5759},"误区四kafka-事务可以自动覆盖数据库","误区四：Kafka 事务可以自动覆盖数据库",[18,5762,5763],{},"Kafka 事务主要协调 Kafka 内部的消息与 Offset，不会自动与 MySQL 等外部数据库形成同一个原子事务。",[473,5765,5767],{"id":5766},"误区五broker-返回成功就等于所有副本已经物理刷盘","误区五：Broker 返回成功就等于所有副本已经物理刷盘",[18,5769,5770],{},"Kafka 的确认、副本复制和磁盘刷盘是不同概念。高可靠主要依靠多副本和同步副本约束，而不是把每条消息都单独同步刷盘。",[14,5772,5774],{"id":5773},"九监控和运维同样重要","九、监控和运维同样重要",[18,5776,5777],{},"配置正确后，还应持续监控：",[159,5779,5780,5783,5786,5789,5792,5795],{},[162,5781,5782],{},"发送失败率、重试次数和发送延迟。",[162,5784,5785],{},"ISR 收缩、未充分复制分区和离线分区。",[162,5787,5788],{},"Broker 磁盘空间、磁盘延迟和网络异常。",[162,5790,5791],{},"消费积压、消费失败、重平衡次数。",[162,5793,5794],{},"死信消息和补偿任务堆积。",[162,5796,5797],{},"业务事件与最终结果的对账差异。",[18,5799,5800],{},"没有监控和对账，即使消息已经丢失，系统也可能长期无法发现。",[14,5802,5803],{"id":5803},"参考资料",[159,5805,5806,5813,5820,5827,5834],{},[162,5807,5808],{},[119,5809,5812],{"href":5810,"rel":5811},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fconfiguration\u002Fproducer-configs\u002F",[2080],"Apache Kafka Producer Configs",[162,5814,5815],{},[119,5816,5819],{"href":5817,"rel":5818},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fconfiguration\u002Fbroker-configs\u002F",[2080],"Apache Kafka Broker Configs",[162,5821,5822],{},[119,5823,5826],{"href":5824,"rel":5825},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fconfiguration\u002Fconsumer-configs\u002F",[2080],"Apache Kafka Consumer Configs",[162,5828,5829],{},[119,5830,5833],{"href":5831,"rel":5832},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fdesign\u002Fdesign\u002F",[2080],"Apache Kafka Design：Message Delivery Semantics",[162,5835,5836],{},[119,5837,5840],{"href":5838,"rel":5839},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fjavadoc\u002Forg\u002Fapache\u002Fkafka\u002Fclients\u002Fconsumer\u002FKafkaConsumer.html",[2080],"Apache KafkaConsumer JavaDoc",[2111,5842,5843],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":150,"searchDepth":251,"depth":251,"links":5845},[5846,5847,5848,5849,5850,5857,5866,5872,5877,5878,5879,5887,5888],{"id":16,"depth":251,"text":16},{"id":115,"depth":251,"text":115},{"id":132,"depth":251,"text":132},{"id":4656,"depth":251,"text":4657},{"id":4680,"depth":251,"text":4681,"children":5851},[5852,5854,5855,5856],{"id":4684,"depth":260,"text":5853},"1. 使用 acks=all",{"id":4778,"depth":260,"text":4779},{"id":4847,"depth":260,"text":4848},{"id":4937,"depth":260,"text":4938},{"id":4965,"depth":251,"text":4966,"children":5858},[5859,5860,5861,5863,5864],{"id":4969,"depth":260,"text":4970},{"id":4988,"depth":260,"text":4989},{"id":5018,"depth":260,"text":5862},"3. 配置 min.insync.replicas",{"id":5044,"depth":260,"text":5045},{"id":5088,"depth":260,"text":5865},"5. acks=all 不等于每条消息都立即 fsync",{"id":5106,"depth":251,"text":5107,"children":5867},[5868,5869,5870,5871],{"id":5110,"depth":260,"text":5111},{"id":5138,"depth":260,"text":5139},{"id":5267,"depth":260,"text":5268},{"id":5307,"depth":260,"text":5308},{"id":5342,"depth":251,"text":5343,"children":5873},[5874,5875,5876],{"id":5355,"depth":260,"text":5356},{"id":5419,"depth":260,"text":5420},{"id":5457,"depth":260,"text":5458},{"id":5542,"depth":251,"text":5543},{"id":5609,"depth":251,"text":5610},{"id":5723,"depth":251,"text":5724,"children":5880},[5881,5883,5884,5885,5886],{"id":5727,"depth":260,"text":5882},"误区一：配置 acks=all 就绝对不会丢",{"id":5742,"depth":260,"text":5743},{"id":5752,"depth":260,"text":5753},{"id":5759,"depth":260,"text":5760},{"id":5766,"depth":260,"text":5767},{"id":5773,"depth":251,"text":5774},{"id":5803,"depth":251,"text":5803},"wiki:java:kafka-message-loss-prevention","从生产者、Broker 和消费者三段链路分析 Kafka 消息丢失的原因，以及 acks、ISR、副本、手动提交 Offset 和业务幂等的完整保障方案。",{},72,"\u002Fjava\u002Fkafka-message-loss-prevention",{"title":4594,"description":5890},"java\u002Fkafka-message-loss-prevention","2026-07-24","7RYCOV4a1N5fgUG67sSi5WAq6gaBz5gHS16sBwuy4yk",{"id":5899,"title":5900,"body":5901,"commentId":6936,"description":6937,"difficulty":3667,"draft":2148,"extension":2149,"meta":6938,"navigation":756,"order":6939,"path":6940,"section":2153,"seo":6941,"stem":6942,"updated":5896,"__hash__":6943},"java\u002Fjava\u002Fkafka-high-concurrency-performance-availability.md","Kafka 是如何实现高并发、高性能、高可用的",{"type":7,"value":5902,"toc":6891},[5903,5906,5908,5922,5924,5931,5933,5942,5944,5947,5961,5965,5969,5972,5978,5981,5988,5992,5995,5998,6009,6012,6016,6019,6021,6027,6030,6034,6037,6040,6044,6048,6051,6057,6060,6064,6067,6073,6093,6096,6100,6103,6109,6112,6126,6132,6136,6139,6145,6148,6165,6172,6178,6181,6184,6191,6194,6222,6229,6233,6236,6239,6253,6270,6274,6277,6283,6289,6295,6298,6307,6313,6316,6323,6329,6333,6336,6339,6345,6347,6353,6356,6362,6365,6368,6371,6392,6395,6399,6403,6406,6412,6415,6418,6422,6425,6431,6434,6443,6446,6451,6454,6457,6461,6466,6477,6480,6494,6497,6500,6506,6509,6513,6520,6526,6529,6532,6580,6583,6589,6592,6596,6599,6602,6608,6611,6615,6677,6680,6686,6690,6693,6713,6716,6719,6725,6728,6732,6736,6739,6743,6749,6753,6756,6760,6763,6770,6777,6781,6784,6795,6799,6812,6815,6829,6831,6873,6875,6888],[10,5904,5900],{"id":5905},"kafka-是如何实现高并发高性能高可用的",[14,5907,16],{"id":16},[105,5909,5910],{},[18,5911,5912,5913,5916,5917,53,5919,5921],{},"Kafka 的高并发主要依靠分区实现横向扩展：一个 Topic 被拆成多个 Partition，分区 Leader 分散在不同 Broker 上，生产者可以并行写入，消费者组也可以按分区并行消费。高性能主要来自追加写和顺序 I\u002FO、操作系统 Page Cache、生产与消费两端的批处理、批量压缩，以及通过 ",[22,5914,5915],{},"sendfile"," 实现的零拷贝传输。高可用则依靠多副本、Leader\u002FFollower、ISR、故障选主和 KRaft Controller Quorum；生产端再配合 ",[22,5918,4609],{},[22,5920,4774],{}," 和幂等生产，消费端通过 Consumer Group、Offset 与重新分配实现故障恢复。Kafka 的核心设计是在分区粒度上并行，在单分区内部维持有序日志，再用副本机制保障故障后的连续服务。",[18,5923,103],{},[105,5925,5926],{},[18,5927,5928],{},[110,5929,5930],{},"分区提供并行度，顺序日志、批处理和零拷贝提供吞吐量，副本、ISR 与故障选主提供可用性。",[14,5932,115],{"id":115},[18,5934,5935],{},[119,5936,5939],{"href":5937,"target":122,"rel":5938},"\u002Fimages\u002Fwiki\u002Fjava\u002Fkafka-high-concurrency-performance-availability.svg",[124,125],[127,5940],{"src":5937,"alt":5941},"Kafka 高并发、高性能和高可用实现机制总览",[14,5943,132],{"id":132},[18,5945,5946],{},"Kafka 的三个能力不是彼此孤立的：",[159,5948,5949,5952,5955,5958],{},[162,5950,5951],{},"分区既是并发执行和横向扩展的单位，也是副本复制与故障恢复的单位。",[162,5953,5954],{},"批处理既减少网络请求，也把大量小写入转化为更高效的顺序写。",[162,5956,5957],{},"Page Cache 既提升读写性能，也能配合副本机制避免为了可靠性而对每条消息执行同步刷盘。",[162,5959,5960],{},"副本增强了可靠性，但同时会增加网络、磁盘与确认延迟，因此需要在吞吐、延迟和可靠性之间取舍。",[14,5962,5964],{"id":5963},"一高并发通过分区横向扩展","一、高并发：通过分区横向扩展",[473,5966,5968],{"id":5967},"_1-partition-是-kafka-的并行单元","1. Partition 是 Kafka 的并行单元",[18,5970,5971],{},"一个 Topic 可以拆成多个 Partition：",[144,5973,5976],{"className":5974,"code":5975,"language":149,"meta":150},[147],"Topic: order-events\n├── Partition 0 → Leader 在 Broker A\n├── Partition 1 → Leader 在 Broker B\n└── Partition 2 → Leader 在 Broker C\n",[22,5977,5975],{"__ignoreMap":150},[18,5979,5980],{},"不同分区可以由不同 Broker 同时处理，因此整体吞吐不再受限于单台机器。增加 Broker 并合理增加分区后，存储容量、网络带宽和读写能力都可以横向扩展。",[18,5982,5983,5984,5987],{},"Kafka 只保证",[110,5985,5986],{},"单分区内有序","，不保证一个 Topic 的所有分区之间全局有序。这是它获得并行能力的重要前提。",[473,5989,5991],{"id":5990},"_2-生产者直接找到分区-leader","2. 生产者直接找到分区 Leader",[18,5993,5994],{},"生产者会缓存集群元数据，根据消息 Key、显式分区或默认分区策略选择 Partition，然后直接向该分区的 Leader 发送数据，中间不需要额外的统一路由节点。",[18,5996,5997],{},"常见分区方式：",[159,5999,6000,6003,6006],{},[162,6001,6002],{},"指定 Key：相同 Key 通常进入同一分区，适合保证同一订单、用户或账户内的消息顺序。",[162,6004,6005],{},"不指定 Key：默认分区策略会在适当时机选择分区，并尽量形成较大的批次。",[162,6007,6008],{},"自定义分区器：按租户、地区或业务规则路由。",[18,6010,6011],{},"如果 Key 分布不均，大量消息集中到少数分区，就会出现热点分区。此时即使 Broker 很多，整体吞吐仍会被热点 Leader 限制。",[473,6013,6015],{"id":6014},"_3-consumer-group-按分区并行消费","3. Consumer Group 按分区并行消费",[18,6017,6018],{},"同一个 Consumer Group 中，一个 Partition 在同一时刻只分配给一个 Consumer；一个 Consumer 可以负责多个 Partition。",[18,6020,686],{},[144,6022,6025],{"className":6023,"code":6024,"language":149,"meta":150},[147],"4 个 Partition + 2 个 Consumer\n→ 每个 Consumer 负责约 2 个 Partition\n\n4 个 Partition + 4 个 Consumer\n→ 每个 Consumer 负责约 1 个 Partition\n\n4 个 Partition + 6 个 Consumer\n→ 最多只有 4 个 Consumer 能获得 Partition\n",[22,6026,6024],{"__ignoreMap":150},[18,6028,6029],{},"因此，一个消费组的有效并行度上限通常受 Partition 数量约束。简单增加 Consumer，而不增加 Partition，不一定能提高吞吐量。",[473,6031,6033],{"id":6032},"_4-broker-内部通过线程分工处理请求","4. Broker 内部通过线程分工处理请求",[18,6035,6036],{},"Broker 不会用一个线程串行完成所有网络和磁盘工作。网络线程负责接收请求、解析协议和返回响应，请求处理线程负责执行 Produce、Fetch 等具体逻辑，后台线程负责日志清理、副本同步等任务。",[18,6038,6039],{},"这种职责分离让网络连接管理、业务请求处理和后台维护能够并行执行，避免某类工作完全阻塞其他工作。",[14,6041,6043],{"id":6042},"二高性能把随机小操作变成顺序批量操作","二、高性能：把随机小操作变成顺序批量操作",[473,6045,6047],{"id":6046},"_1-顺序追加写","1. 顺序追加写",[18,6049,6050],{},"Kafka 的 Partition 本质上是一条只追加的日志。新消息写到日志末尾，不需要像通用数据库那样频繁进行随机位置更新。",[144,6052,6055],{"className":6053,"code":6054,"language":149,"meta":150},[147],"旧数据 ────────────────→ 日志末尾追加新批次\noffset 0  1  2  3  4  5  6 ...\n",[22,6056,6054],{"__ignoreMap":150},[18,6058,6059],{},"顺序 I\u002FO 能充分利用磁盘和操作系统的预读、合并写能力。日志达到一定大小或时间后会切分为新的 Segment，旧 Segment 可以按保留策略清理或压缩，不影响当前日志继续追加。",[473,6061,6063],{"id":6062},"_2-分段日志与稀疏索引","2. 分段日志与稀疏索引",[18,6065,6066],{},"每个 Partition 由多个日志 Segment 组成，常见文件包括：",[144,6068,6071],{"className":6069,"code":6070,"language":149,"meta":150},[147],"00000000000000000000.log\n00000000000000000000.index\n00000000000000000000.timeindex\n",[22,6072,6070],{"__ignoreMap":150},[159,6074,6075,6081,6087],{},[162,6076,6077,6080],{},[22,6078,6079],{},".log"," 保存消息批次。",[162,6082,6083,6086],{},[22,6084,6085],{},".index"," 保存相对 Offset 到日志物理位置的稀疏映射。",[162,6088,6089,6092],{},[22,6090,6091],{},".timeindex"," 保存时间戳到相对 Offset 的稀疏映射。",[18,6094,6095],{},"查找消息时，Kafka 先定位 Segment，再通过稀疏索引找到接近目标的位置，最后在日志中继续顺序查找。索引不需要记录每一条消息，因此能控制索引体积和内存占用。",[473,6097,6099],{"id":6098},"_3-充分利用-page-cache","3. 充分利用 Page Cache",[18,6101,6102],{},"Kafka 不把全部消息封装成大量 Java 堆对象长期缓存，而是依赖操作系统 Page Cache：",[144,6104,6107],{"className":6105,"code":6106,"language":149,"meta":150},[147],"Producer\n→ Broker 写日志文件\n→ 数据先进入 Page Cache\n→ 操作系统异步回写磁盘\n",[22,6108,6106],{"__ignoreMap":150},[18,6110,6111],{},"这样做有几个好处：",[159,6113,6114,6117,6120,6123],{},[162,6115,6116],{},"利用操作系统成熟的预读和回写机制。",[162,6118,6119],{},"减少 JVM 堆内存占用和 GC 压力。",[162,6121,6122],{},"热数据可以直接从 Page Cache 读取。",[162,6124,6125],{},"Broker 重启后，操作系统缓存仍可能保留部分热数据，不必完全重新构建 JVM 内缓存。",[18,6127,6128,6129,6131],{},"Kafka 的可靠性主要依赖多副本，而不是对每条消息都执行一次同步 ",[22,6130,5095],{},"。如果强制每条消息同步刷盘，吞吐和延迟都会明显恶化。",[473,6133,6135],{"id":6134},"_4-端到端批处理","4. 端到端批处理",[18,6137,6138],{},"批处理贯穿生产、Broker 存储和消费三段链路：",[144,6140,6143],{"className":6141,"code":6142,"language":149,"meta":150},[147],"多条消息\n→ Producer 聚合成 Record Batch\n→ 一次网络请求发送\n→ Broker 将 Record Batch 连续追加到分区日志\n→ Consumer 一次 Fetch 多条消息\n",[22,6144,6142],{"__ignoreMap":150},[18,6146,6147],{},"它可以摊薄：",[159,6149,6150,6153,6156,6159,6162],{},[162,6151,6152],{},"网络往返成本。",[162,6154,6155],{},"系统调用成本。",[162,6157,6158],{},"协议头和请求头开销。",[162,6160,6161],{},"磁盘写入次数。",[162,6163,6164],{},"消息压缩成本。",[18,6166,6167,6168,6171],{},"这里的“Broker 追加批次”不是把多条消息合并成一条业务消息，也不承诺底层一定只发生一次系统调用。它表示 Kafka 的生产者和存储格式都以 ",[110,6169,6170],{},"Record Batch"," 为基本单位：",[144,6173,6176],{"className":6174,"code":6175,"language":149,"meta":150},[147],"Record Batch\n├── 批次头：baseOffset、长度、时间戳、压缩类型、CRC 等\n├── Record 1\n├── Record 2\n└── Record 3\n",[22,6177,6175],{"__ignoreMap":150},[18,6179,6180],{},"Producer 会先按 Partition 在内存中积累消息，形成 Record Batch。Broker 收到后，对批次进行校验，为其中的消息确定 Offset，然后把整段连续数据追加到对应 Partition 的当前日志 Segment 末尾。相比每条消息都单独发请求、单独写日志，批次追加可以用更少的请求和更大的连续 I\u002FO 摊薄固定开销。",[18,6182,6183],{},"一个 ProduceRequest 还可以携带多个 Partition 的 Record Batch，但 Broker 最终仍然分别追加到各个 Partition 的日志中。因此：",[105,6185,6186],{},[18,6187,6188],{},[110,6189,6190],{},"批次是网络传输和日志写入的基本单位，Partition 才是日志组织、顺序保证和副本复制的基本单位。",[18,6192,6193],{},"生产者常见相关参数：",[144,6195,6197],{"className":4755,"code":6196,"language":4757,"meta":150,"style":150},"batch.size=16384\nlinger.ms=5\ncompression.type=lz4\n",[22,6198,6199,6207,6214],{"__ignoreMap":150},[203,6200,6201,6204],{"class":205,"line":206},[203,6202,6203],{"class":209},"batch.size",[203,6205,6206],{"class":219},"=16384\n",[203,6208,6209,6212],{"class":205,"line":251},[203,6210,6211],{"class":209},"linger.ms",[203,6213,4817],{"class":219},[203,6215,6216,6219],{"class":205,"line":260},[203,6217,6218],{"class":209},"compression.type",[203,6220,6221],{"class":219},"=lz4\n",[18,6223,6224,40,6226,6228],{},[22,6225,6203],{},[22,6227,6211],{}," 体现的是吞吐与延迟之间的取舍：适度等待可以积累更大的批次，但等待过长会增加低流量场景的发送延迟。参数值应根据消息大小、流量和延迟目标压测确定，不能机械照搬示例。",[473,6230,6232],{"id":6231},"_5-批量压缩","5. 批量压缩",[18,6234,6235],{},"Kafka 对一个 Record Batch 进行整体压缩，而不是分别压缩每条消息。相似消息中的字段名和公共内容可以获得更好的压缩率。",[18,6237,6238],{},"压缩后的批次会以压缩形式写入日志并传输给消费者，从而降低：",[159,6240,6241,6244,6247,6250],{},[162,6242,6243],{},"Broker 磁盘占用。",[162,6245,6246],{},"生产者到 Broker 的网络流量。",[162,6248,6249],{},"副本复制流量。",[162,6251,6252],{},"Broker 到消费者的网络流量。",[18,6254,6255,6256,53,6259,53,6262,6265,6266,6269],{},"压缩会消耗 CPU，因此需要根据资源瓶颈选择 ",[22,6257,6258],{},"lz4",[22,6260,6261],{},"zstd",[22,6263,6264],{},"snappy"," 或 ",[22,6267,6268],{},"gzip"," 等算法。网络或磁盘是瓶颈时，压缩往往能换取更高吞吐。",[473,6271,6273],{"id":6272},"_6-零拷贝","6. 零拷贝",[18,6275,6276],{},"消费者拉取日志数据时，普通路径可能需要：",[144,6278,6281],{"className":6279,"code":6280,"language":149,"meta":150},[147],"磁盘 → Page Cache → 用户空间 → Socket Buffer → 网卡\n",[22,6282,6280],{"__ignoreMap":150},[18,6284,6285,6286,6288],{},"Kafka 在适用条件下利用 Linux ",[22,6287,5915],{},"，让数据从 Page Cache 直接进入网络发送路径，减少用户态和内核态之间的复制与上下文切换：",[144,6290,6293],{"className":6291,"code":6292,"language":149,"meta":150},[147],"Page Cache → Socket \u002F 网卡\n",[22,6294,6292],{"__ignoreMap":150},[18,6296,6297],{},"这就是常说的零拷贝。它主要优化 Broker 向消费者传输已有日志数据的过程。",[18,6299,6300],{},[119,6301,6304],{"href":6302,"target":122,"rel":6303},"\u002Fimages\u002Fwiki\u002Fjava\u002Fkafka-sendfile-vs-traditional-io.svg",[124,125],[127,6305],{"src":6302,"alt":6306},"Kafka sendfile 零拷贝与传统文件传输路径对比",[18,6308,6309,6310,6312],{},"传统路径需要先把数据从内核 Page Cache 复制到 Broker 的用户空间，再从用户空间写回内核 Socket Buffer；",[22,6311,5915],{}," 让内核直接组织 Page Cache 到网络 Socket 的传输，Broker 不再把消息内容完整搬入 JVM 用户空间。",[18,6314,6315],{},"所以“零拷贝”更准确的含义是：",[105,6317,6318],{},[18,6319,6320],{},[110,6321,6322],{},"减少或绕过 Page Cache 与用户空间之间不必要的数据复制，而不是数据在磁盘、内存和网卡之间一次都不移动。",[18,6324,6325,6326,6328],{},"需要注意，Kafka 官方文档明确说明，启用 SSL 时数据要经过用户态 TLS 处理，当前不会使用这条 ",[22,6327,5915],{}," 路径。因此不能笼统地说所有 Kafka 网络传输都一定是零拷贝。",[473,6330,6332],{"id":6331},"_7-consumer-pull-与长轮询","7. Consumer Pull 与长轮询",[18,6334,6335],{},"Kafka 由消费者主动 Pull 数据。消费者可以根据自己的处理能力决定拉取速度，处理不过来时，未消费消息会暂时保留在 Kafka 中，并表现为 Consumer Lag 增长，而不是由 Broker 持续 Push 直至压垮消费者。",[18,6337,6338],{},"Consumer Lag 表示消费者的消费进度落后于 Partition 最新位置的程度。监控消费组时，通常可以近似理解为：",[144,6340,6343],{"className":6341,"code":6342,"language":149,"meta":150},[147],"Consumer Lag\n= Partition Log End Offset\n- Consumer Group 已提交 Offset\n",[22,6344,6342],{"__ignoreMap":150},[18,6346,686],{},[144,6348,6351],{"className":6349,"code":6350,"language":149,"meta":150},[147],"Partition 下一条待写位置：10000\n消费组下一条待消费位置：9400\n\nLag = 10000 - 9400 = 600\n",[22,6352,6350],{"__ignoreMap":150},[18,6354,6355],{},"这表示当前大约还有 600 个 Offset 的数据尚未被该消费组处理。Lag 不是消息丢失，而是消费积压：",[144,6357,6360],{"className":6358,"code":6359,"language":149,"meta":150},[147],"生产速度 > 消费速度\n→ Lag 持续增大\n\n消费速度 > 生产速度\n→ Consumer 逐步追赶\n→ Lag 逐步减小\n",[22,6361,6359],{"__ignoreMap":150},[18,6363,6364],{},"Lag 不能只看某一时刻的绝对值，还要观察增长趋势、积压持续时间和业务允许的最大延迟。同样数量的 Lag，在每秒几条和每秒几十万条的 Topic 中代表的时间延迟完全不同。",[18,6366,6367],{},"如果 Lag 长期增长，可能是消费者处理变慢、下游故障、分区热点、Consumer 数量不足或频繁重平衡。若积压时间超过 Topic 的数据保留期限，旧消息可能已经被清理，消费者就不能再依靠 Kafka 把这部分数据完整追回。",[18,6369,6370],{},"Fetch 请求支持批量拉取和长轮询：",[144,6372,6374],{"className":4755,"code":6373,"language":4757,"meta":150,"style":150},"fetch.min.bytes=1\nfetch.max.wait.ms=500\n",[22,6375,6376,6384],{"__ignoreMap":150},[203,6377,6378,6381],{"class":205,"line":206},[203,6379,6380],{"class":209},"fetch.min.bytes",[203,6382,6383],{"class":219},"=1\n",[203,6385,6386,6389],{"class":205,"line":251},[203,6387,6388],{"class":209},"fetch.max.wait.ms",[203,6390,6391],{"class":219},"=500\n",[18,6393,6394],{},"Broker 暂时没有足够数据时，可以等待一段时间再响应，避免消费者无数据时频繁空轮询；流量较高时，又可以一次返回较大的批次。",[14,6396,6398],{"id":6397},"三高可用副本isr-与故障转移","三、高可用：副本、ISR 与故障转移",[473,6400,6402],{"id":6401},"_1-每个-partition-可以有多个副本","1. 每个 Partition 可以有多个副本",[18,6404,6405],{},"假设副本因子为 3：",[144,6407,6410],{"className":6408,"code":6409,"language":149,"meta":150},[147],"Partition 0\n├── Leader：Broker A\n├── Follower：Broker B\n└── Follower：Broker C\n",[22,6411,6409],{"__ignoreMap":150},[18,6413,6414],{},"生产和普通消费请求由 Leader 处理，Follower 分别从 Leader 拉取并复制日志。多个 Follower 的同步是彼此独立的，不存在先同步 B、再由 B 同步 C 的固定顺序。",[18,6416,6417],{},"副本应尽量分散在不同 Broker；具备机架感知配置时，还可以跨机架放置，降低单机或单机架故障带来的影响。",[473,6419,6421],{"id":6420},"_2-isr-维护可安全选主的副本集合","2. ISR 维护可安全选主的副本集合",[18,6423,6424],{},"ISR 是 In-Sync Replicas，即当前与 Leader 保持同步的副本集合。Follower 故障或长时间落后时会被移出 ISR，重新追上后可以再次加入。",[144,6426,6429],{"className":6427,"code":6428,"language":149,"meta":150},[147],"正常：ISR = [A, B, C]\nC 落后：ISR = [A, B]\n",[22,6430,6428],{"__ignoreMap":150},[18,6432,6433],{},"Leader 故障后，Controller 会从符合条件的副本中选择新的 Leader。以 ISR 为基础进行选主，可以避免把明显缺少最新日志的副本直接作为新的数据源。",[473,6435,6437,6438,6440,6441],{"id":6436},"_3-acks-与-mininsyncreplicas","3. ",[22,6439,4693],{}," 与 ",[22,6442,4774],{},[18,6444,6445],{},"常见的高可靠组合是：",[144,6447,6449],{"className":6448,"code":5028,"language":149,"meta":150},[147],[22,6450,5028],{"__ignoreMap":150},[18,6452,6453],{},"含义是生产者等待当前 ISR 的确认，并且 ISR 至少要有两个副本，否则 Broker 拒绝写入。",[18,6455,6456],{},"这个配置体现了 CAP 取舍：副本不足时拒绝写入，会降低部分可用性，但可以避免在只剩单副本时继续写入并承担更高的数据丢失风险。",[473,6458,6460],{"id":6459},"_4-kraft-controller-quorum-保证控制面可用","4. KRaft Controller Quorum 保证控制面可用",[6462,6463,6465],"h4",{"id":6464},"kraft-controller-quorum-是什么","KRaft Controller Quorum 是什么",[18,6467,6468,6469,6472,6473,6476],{},"KRaft 是 Kafka 自己实现的元数据管理机制，用来替代早期依赖的 ZooKeeper。集群中会有若干个承担 ",[22,6470,6471],{},"controller"," 角色的节点，这些 Controller 组成一个基于 Raft 共识协议的 ",[110,6474,6475],{},"Controller Quorum","，即控制器仲裁组。",[18,6478,6479],{},"它们维护的是集群元数据，而不是普通 Topic 的业务消息：",[159,6481,6482,6485,6488,6491],{},[162,6483,6484],{},"Broker 注册、存活状态和节点信息。",[162,6486,6487],{},"Topic 与 Partition 的定义。",[162,6489,6490],{},"每个 Partition 的副本分配、Leader 和 ISR。",[162,6492,6493],{},"配置、配额以及其他集群级元数据。",[18,6495,6496],{},"这些变化会写入 KRaft 的集群元数据日志，并复制给 Quorum 中的其他 Controller。一次元数据变更获得多数 Controller 确认后才算提交，因此单个 Controller 故障不会导致已提交元数据丢失。",[18,6498,6499],{},"常见部署是 3 个或 5 个 Controller：",[144,6501,6504],{"className":6502,"code":6503,"language":149,"meta":150},[147],"3 个 Controller → 多数派为 2，可容忍 1 个故障\n5 个 Controller → 多数派为 3，可容忍 2 个故障\n",[22,6505,6503],{"__ignoreMap":150},[18,6507,6508],{},"如果存活 Controller 失去多数派，集群不能安全提交新的元数据变更。此时已有数据读写是否还能短暂继续，取决于具体操作和现有元数据状态，但创建 Topic、Partition Leader 调度等控制面操作会受到影响。",[6462,6510,6512],{"id":6511},"active-controller-是什么","Active Controller 是什么",[18,6514,6515,6516,6519],{},"Controller Quorum 在同一时刻会选出一个 Leader，Kafka 的文档和运行语境中通常称它为 ",[110,6517,6518],{},"Active Controller","。其他 Controller 作为 Follower 复制元数据日志，随时准备接管。",[144,6521,6524],{"className":6522,"code":6523,"language":149,"meta":150},[147],"Controller A：Active Controller\n├── 接收和处理元数据变更\n├── 管理 Broker 注册与故障\n├── 触发 Partition Leader 选举\n└── 把变更写入元数据日志\n\nController B、C：Follower Controllers\n├── 复制元数据日志\n└── Active Controller 故障后参与重新选举\n",[22,6525,6523],{"__ignoreMap":150},[18,6527,6528],{},"Active Controller 不是所有业务消息的入口，也不负责替 Broker 保存普通 Topic 数据。生产者和消费者仍然直接与 Partition Leader 所在的 Broker 通信。",[18,6530,6531],{},"需要明确区分两个层面：",[299,6533,6534,6550],{},[302,6535,6536],{},[305,6537,6538,6541,6544,6547],{},[308,6539,6540],{},"层面",[308,6542,6543],{},"主要角色",[308,6545,6546],{},"保存什么",[308,6548,6549],{},"负责什么",[315,6551,6552,6566],{},[305,6553,6554,6557,6560,6563],{},[320,6555,6556],{},"数据面",[320,6558,6559],{},"Broker、Partition Leader\u002FFollower",[320,6561,6562],{},"普通 Topic 的业务消息",[320,6564,6565],{},"生产、消费、副本复制",[305,6567,6568,6571,6574,6577],{},[320,6569,6570],{},"控制面",[320,6572,6573],{},"KRaft Controller Quorum",[320,6575,6576],{},"集群元数据日志",[320,6578,6579],{},"节点管理、元数据变更、Leader 调度",[18,6581,6582],{},"例如 Broker A 突然故障：",[144,6584,6587],{"className":6585,"code":6586,"language":149,"meta":150},[147],"Active Controller 感知 Broker A 失联\n→ 找出受影响的 Partition\n→ 从符合条件的副本中选择新 Leader\n→ 将选举结果写入元数据日志\n→ 多数 Controller 确认提交\n→ 向相关 Broker 发布新元数据\n→ 客户端刷新元数据并访问新 Leader\n",[22,6588,6586],{"__ignoreMap":150},[18,6590,6591],{},"如果 Active Controller 自身故障，剩余 Controller 会通过 Raft 重新选出新的 Active Controller。新节点已经复制了已提交的元数据日志，因此可以继续管理集群。",[473,6593,6595],{"id":6594},"_5-consumer-group-自动接管故障消费者的分区","5. Consumer Group 自动接管故障消费者的分区",[18,6597,6598],{},"消费者通过心跳维持组成员身份。如果某个 Consumer 崩溃或超时，Consumer Group 会重新分配它原来负责的 Partition，让其他 Consumer 接管。",[18,6600,6601],{},"消费进度保存在已提交的 Offset 中，因此新 Consumer 可以从相应位置继续处理：",[144,6603,6606],{"className":6604,"code":6605,"language":149,"meta":150},[147],"Consumer A 故障\n→ 触发重新分配\n→ Partition 交给 Consumer B\n→ B 从已提交 Offset 继续消费\n",[22,6607,6605],{"__ignoreMap":150},[18,6609,6610],{},"重平衡期间可能产生短暂停顿，所以高可用不等于无感切换。合理设置会话、心跳、处理时长，并避免频繁扩缩容，有助于减少重平衡影响。",[14,6612,6614],{"id":6613},"四三种能力如何协同","四、三种能力如何协同",[299,6616,6617,6633],{},[302,6618,6619],{},[305,6620,6621,6624,6627,6630],{},[308,6622,6623],{},"目标",[308,6625,6626],{},"核心机制",[308,6628,6629],{},"解决的问题",[308,6631,6632],{},"主要代价",[315,6634,6635,6649,6663],{},[305,6636,6637,6640,6643,6646],{},[320,6638,6639],{},"高并发",[320,6641,6642],{},"多 Partition、多 Broker、Consumer Group",[320,6644,6645],{},"并行生产、存储和消费",[320,6647,6648],{},"分区与元数据管理成本",[305,6650,6651,6654,6657,6660],{},[320,6652,6653],{},"高性能",[320,6655,6656],{},"顺序写、Page Cache、批处理、压缩、零拷贝",[320,6658,6659],{},"降低 I\u002FO、复制和网络开销",[320,6661,6662],{},"批处理延迟与 CPU 消耗",[305,6664,6665,6668,6671,6674],{},[320,6666,6667],{},"高可用",[320,6669,6670],{},"多副本、ISR、Controller Quorum、故障转移",[320,6672,6673],{},"节点故障后继续提供服务",[320,6675,6676],{},"副本存储、网络与确认延迟",[18,6678,6679],{},"可以把整个链路理解为：",[144,6681,6684],{"className":6682,"code":6683,"language":149,"meta":150},[147],"消息按 Partition 分流\n→ Producer 批量发送\n→ Leader 顺序追加到日志\n→ Follower 并行复制\n→ 满足确认条件后返回\n→ Consumer Group 按 Partition 批量拉取\n",[22,6685,6683],{"__ignoreMap":150},[14,6687,6689],{"id":6688},"五为什么不能只靠增加分区提升性能","五、为什么不能只靠增加分区提升性能",[18,6691,6692],{},"分区是扩展能力的基础，但分区不是越多越好。分区数量增加会同时带来：",[159,6694,6695,6698,6701,6704,6707,6710],{},[162,6696,6697],{},"更多日志目录、Segment 和文件句柄。",[162,6699,6700],{},"更多副本复制任务。",[162,6702,6703],{},"更大的集群元数据。",[162,6705,6706],{},"更高的 Leader 选举和故障恢复成本。",[162,6708,6709],{},"Consumer Group 重平衡开销。",[162,6711,6712],{},"单 Key 顺序范围更难调整。",[18,6714,6715],{},"合理做法是根据目标吞吐量、单分区压测能力、消费者并行度、副本数和未来增长空间估算，而不是一次创建大量分区。",[18,6717,6718],{},"一个简化估算思路：",[144,6720,6723],{"className":6721,"code":6722,"language":149,"meta":150},[147],"分区数 ≥ max(\n  目标生产吞吐 \u002F 单分区生产吞吐,\n  目标消费吞吐 \u002F 单消费者单分区吞吐,\n  期望消费并行度\n)\n",[22,6724,6722],{"__ignoreMap":150},[18,6726,6727],{},"最终仍需要在接近生产环境的机器、网络、副本配置和消息大小下进行压测。",[14,6729,6731],{"id":6730},"六常见误区","六、常见误区",[473,6733,6735],{"id":6734},"误区一kafka-使用磁盘所以一定比内存消息队列慢","误区一：Kafka 使用磁盘，所以一定比内存消息队列慢",[18,6737,6738],{},"Kafka 采用顺序追加、Page Cache 和批处理，大量读写实际在操作系统缓存中完成。性能取决于访问模式，而不是简单由“使用磁盘”决定。",[473,6740,6742],{"id":6741},"误区二零拷贝让数据完全不发生复制","误区二：零拷贝让数据完全不发生复制",[18,6744,6745,6746,6748],{},"零拷贝是减少不必要的用户态复制，并非物理意义上的一次复制都没有；而且启用 SSL 时不会走 Kafka 文档描述的 ",[22,6747,5915],{}," 路径。",[473,6750,6752],{"id":6751},"误区三消费者越多消费速度一定越快","误区三：消费者越多，消费速度一定越快",[18,6754,6755],{},"同一消费组内，一个 Partition 同时只能交给一个 Consumer。Consumer 数超过 Partition 数后，多出的 Consumer 不会获得分区。",[473,6757,6759],{"id":6758},"误区四副本越多高可用越高且没有代价","误区四：副本越多，高可用越高且没有代价",[18,6761,6762],{},"更多副本会增加存储、网络复制和写确认成本。副本数应根据故障域和可靠性目标确定，常见三副本不代表所有场景都必须相同。",[473,6764,6766,6767,6769],{"id":6765},"误区五acksall-就代表所有配置副本都已写入","误区五：",[22,6768,4609],{}," 就代表所有配置副本都已写入",[18,6771,6772,6774,6775,33],{},[22,6773,4609],{}," 等待的是当前 ISR，而不是所有配置副本。要避免 ISR 只剩一个副本时仍然成功写入，需要配合 ",[22,6776,4774],{},[14,6778,6780],{"id":6779},"七排查性能和可用性问题时看什么","七、排查性能和可用性问题时看什么",[473,6782,6783],{"id":6783},"生产端",[159,6785,6786,6789,6792],{},[162,6787,6788],{},"发送吞吐、请求延迟、错误率和重试次数。",[162,6790,6791],{},"批次大小、压缩率和缓冲区等待。",[162,6793,6794],{},"消息 Key 是否造成分区流量倾斜。",[473,6796,6798],{"id":6797},"broker","Broker",[159,6800,6801,6804,6807,6809],{},[162,6802,6803],{},"各 Broker、Partition 和 Leader 的流量是否均衡。",[162,6805,6806],{},"磁盘利用率、磁盘延迟、网络带宽和请求队列。",[162,6808,5785],{},[162,6810,6811],{},"Page Cache 命中效果和系统是否发生 Swap。",[473,6813,6814],{"id":6814},"消费端",[159,6816,6817,6820,6823,6826],{},[162,6818,6819],{},"Consumer Lag 及其增长速度。",[162,6821,6822],{},"单条消息处理耗时和批次处理耗时。",[162,6824,6825],{},"Consumer 数量与 Partition 数量是否匹配。",[162,6827,6828],{},"重平衡次数、心跳超时和 Offset 提交失败。",[14,6830,5803],{"id":5803},[159,6832,6833,6839,6846,6853,6858,6863,6868],{},[162,6834,6835],{},[119,6836,6838],{"href":5831,"rel":6837},[2080],"Apache Kafka Design",[162,6840,6841],{},[119,6842,6845],{"href":6843,"rel":6844},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Foperations\u002Fkraft\u002F",[2080],"Apache Kafka KRaft",[162,6847,6848],{},[119,6849,6852],{"href":6850,"rel":6851},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fimplementation\u002Fmessage-format\u002F",[2080],"Apache Kafka Message Format",[162,6854,6855],{},[119,6856,5812],{"href":5810,"rel":6857},[2080],[162,6859,6860],{},[119,6861,5826],{"href":5824,"rel":6862},[2080],[162,6864,6865],{},[119,6866,5819],{"href":5817,"rel":6867},[2080],[162,6869,6870],{},[119,6871,5840],{"href":5838,"rel":6872},[2080],[14,6874,3596],{"id":3596},[159,6876,6877,6882],{},[162,6878,6879],{},[119,6880,4594],{"href":6881},"\u002Fwiki\u002Fjava\u002Fkafka-message-loss-prevention\u002F",[162,6883,6884],{},[119,6885,6887],{"href":6886},"\u002Fwiki\u002Fjava\u002Fkafka-message-backlog-handling\u002F","Kafka 消息积压如何处理",[2111,6889,6890],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":150,"searchDepth":251,"depth":251,"links":6892},[6893,6894,6895,6896,6902,6911,6919,6920,6921,6929,6934,6935],{"id":16,"depth":251,"text":16},{"id":115,"depth":251,"text":115},{"id":132,"depth":251,"text":132},{"id":5963,"depth":251,"text":5964,"children":6897},[6898,6899,6900,6901],{"id":5967,"depth":260,"text":5968},{"id":5990,"depth":260,"text":5991},{"id":6014,"depth":260,"text":6015},{"id":6032,"depth":260,"text":6033},{"id":6042,"depth":251,"text":6043,"children":6903},[6904,6905,6906,6907,6908,6909,6910],{"id":6046,"depth":260,"text":6047},{"id":6062,"depth":260,"text":6063},{"id":6098,"depth":260,"text":6099},{"id":6134,"depth":260,"text":6135},{"id":6231,"depth":260,"text":6232},{"id":6272,"depth":260,"text":6273},{"id":6331,"depth":260,"text":6332},{"id":6397,"depth":251,"text":6398,"children":6912},[6913,6914,6915,6917,6918],{"id":6401,"depth":260,"text":6402},{"id":6420,"depth":260,"text":6421},{"id":6436,"depth":260,"text":6916},"3. acks 与 min.insync.replicas",{"id":6459,"depth":260,"text":6460},{"id":6594,"depth":260,"text":6595},{"id":6613,"depth":251,"text":6614},{"id":6688,"depth":251,"text":6689},{"id":6730,"depth":251,"text":6731,"children":6922},[6923,6924,6925,6926,6927],{"id":6734,"depth":260,"text":6735},{"id":6741,"depth":260,"text":6742},{"id":6751,"depth":260,"text":6752},{"id":6758,"depth":260,"text":6759},{"id":6765,"depth":260,"text":6928},"误区五：acks=all 就代表所有配置副本都已写入",{"id":6779,"depth":251,"text":6780,"children":6930},[6931,6932,6933],{"id":6783,"depth":260,"text":6783},{"id":6797,"depth":260,"text":6798},{"id":6814,"depth":260,"text":6814},{"id":5803,"depth":251,"text":5803},{"id":3596,"depth":251,"text":3596},"wiki:java:kafka-high-concurrency-performance-availability","从分区并行、顺序写、Page Cache、批处理、零拷贝、副本与 ISR 等机制，系统理解 Kafka 高吞吐与高可用架构。",{},73,"\u002Fjava\u002Fkafka-high-concurrency-performance-availability",{"title":5900,"description":6937},"java\u002Fkafka-high-concurrency-performance-availability","qNGjCN5U7STnjw0cnfrIs6bM77HfEAuGKTcJPBrUgY8",{"id":6945,"title":6887,"body":6946,"commentId":8298,"description":8299,"difficulty":3667,"draft":2148,"extension":2149,"meta":8300,"navigation":756,"order":8301,"path":8302,"section":2153,"seo":8303,"stem":8304,"updated":5896,"__hash__":8305},"java\u002Fjava\u002Fkafka-message-backlog-handling.md",{"type":7,"value":6947,"toc":8251},[6948,6951,6953,6958,6960,6967,6969,6978,6980,6983,6989,6992,6996,6999,7002,7007,7009,7015,7018,7021,7092,7095,7098,7104,7107,7113,7116,7119,7123,7127,7133,7136,7140,7143,7166,7173,7177,7180,7200,7203,7209,7212,7216,7219,7222,7228,7231,7235,7238,7255,7258,7262,7266,7269,7275,7278,7282,7285,7299,7302,7305,7311,7314,7318,7321,7324,7327,7331,7335,7338,7341,7347,7350,7354,7357,7380,7382,7388,7393,7396,7402,7405,7408,7425,7428,7432,7435,7474,7477,7502,7506,7509,7515,7518,7524,7527,7537,7540,7546,7549,7557,7571,7580,7584,7597,7600,7606,7610,7613,7619,7622,7628,7631,7634,7637,7640,7643,7674,7677,7681,7685,7688,7691,7698,7701,7705,7708,7711,7717,7720,7726,7729,7732,7737,7740,7743,7747,7753,7756,7760,7766,7772,7775,7778,7792,7796,7799,7805,7808,7812,7815,7818,7824,7827,7847,7850,7854,7857,7863,7866,7880,7883,7887,7890,7896,7899,7905,7908,7914,7916,7922,7925,7931,7935,7938,7945,7948,7965,7968,7972,7975,7978,8035,8042,8095,8098,8101,8107,8113,8116,8119,8125,8128,8132,8135,8158,8162,8166,8169,8176,8179,8183,8186,8190,8197,8200,8204,8207,8209,8235,8237,8248],[10,6949,6887],{"id":6950},"kafka-消息积压如何处理",[14,6952,16],{"id":16},[105,6954,6955],{},[18,6956,6957],{},"Kafka 消息积压不能一上来就盲目增加消费者，首先要确认积压范围和原因：观察各 Partition 的 Consumer Lag、生产速率、消费速率、消费者存活状态、重平衡、处理耗时和下游依赖。如果消费实例异常或下游故障，应先恢复服务；如果生产速度持续大于消费速度，需要对非核心生产流量限流，同时扩容消费者，但同一消费组的有效并行度不会超过 Partition 数量。如果单条处理过慢，应通过批量写库、减少同步 RPC、优化慢 SQL 或使用受控的异步处理提高单实例吞吐；如果只有个别 Partition 积压，要检查消息 Key 是否造成热点；如果被异常消息反复阻塞，需要有限重试后进入死信或人工补偿。恢复时间可以用“积压量 ÷（消费速率－生产速率）”估算，只有消费能力大于生产速度，积压才会真正下降。Offset 跳到最新只能在业务明确同意丢弃旧消息时使用，不能作为常规解决方案。",[18,6959,103],{},[105,6961,6962],{},[18,6963,6964],{},[110,6965,6966],{},"先判断为什么消费速度落后，再从减少流入、恢复异常、提升单实例吞吐和增加有效并行度四个方向处理。",[14,6968,115],{"id":115},[18,6970,6971],{},[119,6972,6975],{"href":6973,"target":122,"rel":6974},"\u002Fimages\u002Fwiki\u002Fjava\u002Fkafka-message-backlog-handling.svg",[124,125],[127,6976],{"src":6973,"alt":6977},"Kafka 消息积压排查与处理流程",[14,6979,132],{"id":132},[18,6981,6982],{},"Kafka 消息积压的本质是：",[144,6984,6987],{"className":6985,"code":6986,"language":149,"meta":150},[147],"一段时间内的生产速度\n>\n一段时间内的有效消费速度\n",[22,6988,6986],{"__ignoreMap":150},[18,6990,6991],{},"积压不是单一故障类型，它可能由突发流量、消费者异常、下游变慢、分区热点或异常消息阻塞引起。不同原因的处理方式完全不同。",[14,6993,6995],{"id":6994},"一什么是-kafka-消息积压","一、什么是 Kafka 消息积压",[18,6997,6998],{},"每个 Partition 中的消息都有 Offset。生产者持续向日志末尾写入，消费组通过已提交 Offset 记录自己的处理进度。",[18,7000,7001],{},"监控消费组时，Consumer Lag 通常可以近似理解为：",[144,7003,7005],{"className":7004,"code":6342,"language":149,"meta":150},[147],[22,7006,6342],{"__ignoreMap":150},[18,7008,686],{},[144,7010,7013],{"className":7011,"code":7012,"language":149,"meta":150},[147],"Partition Log End Offset：120000\n消费组已提交 Offset：95000\n\nLag = 120000 - 95000 = 25000\n",[22,7014,7012],{"__ignoreMap":150},[18,7016,7017],{},"表示这个消费组在该 Partition 上大约落后 25000 个 Offset。",[18,7019,7020],{},"需要同时观察三类指标：",[299,7022,7023,7035],{},[302,7024,7025],{},[305,7026,7027,7030,7032],{},[308,7028,7029],{},"指标",[308,7031,2553],{},[308,7033,7034],{},"判断价值",[315,7036,7037,7048,7059,7070,7081],{},[305,7038,7039,7042,7045],{},[320,7040,7041],{},"当前 Lag",[320,7043,7044],{},"还落后多少 Offset",[320,7046,7047],{},"反映积压规模",[305,7049,7050,7053,7056],{},[320,7051,7052],{},"Lag 增长速度",[320,7054,7055],{},"单位时间新增多少积压",[320,7057,7058],{},"判断问题是否正在恶化",[305,7060,7061,7064,7067],{},[320,7062,7063],{},"最老未处理消息年龄",[320,7065,7066],{},"最老待处理消息的时间戳距现在多久",[320,7068,7069],{},"反映业务已经延迟多久",[305,7071,7072,7075,7078],{},[320,7073,7074],{},"积压字节量",[320,7076,7077],{},"待处理消息占用的总字节数",[320,7079,7080],{},"反映网络、磁盘和反序列化压力",[305,7082,7083,7086,7089],{},[320,7084,7085],{},"单条处理耗时",[320,7087,7088],{},"处理一条或一批消息实际需要多久",[320,7090,7091],{},"用于估算积压清空速度",[18,7093,7094],{},"只看 Lag 绝对值容易误判。例如同样积压 10 万条，在每秒消费 20 万条的系统里可能很快恢复，在每秒只消费 100 条的系统里则非常严重。",[18,7096,7097],{},"Lag 是 Offset 数量差，本身只表示“落后多少条记录”，不能直接等同于时间延迟。对于相同的 10 万 Lag，消费者处理速度越快，处理完现有积压所需的时间越短：",[144,7099,7102],{"className":7100,"code":7101,"language":149,"meta":150},[147],"处理现有积压所需时间\n≈ Lag ÷ 消费速度\n",[22,7103,7101],{"__ignoreMap":150},[18,7105,7106],{},"如果生产者仍在持续写入，要估算 Lag 整体降为 0 的时间，则应扣除生产速度：",[144,7108,7111],{"className":7109,"code":7110,"language":149,"meta":150},[147],"Lag 归零时间\n≈ Lag ÷（消费速度－生产速度）\n",[22,7112,7110],{"__ignoreMap":150},[18,7114,7115],{},"只有消费速度大于生产速度，Lag 才会持续下降。至于当前业务已经延迟多久，应直接查看最老未处理消息的时间戳，而不是只根据 Lag 推算。",[18,7117,7118],{},"消息大小影响的是积压字节量、网络传输和反序列化成本；单条处理复杂度影响的是消费速度。它们都很重要，但不能与 Lag 条数或消息年龄混为同一个指标。",[14,7120,7122],{"id":7121},"二先判断积压发生在哪里","二、先判断积压发生在哪里",[473,7124,7126],{"id":7125},"_1-是所有-partition-积压还是少数-partition-积压","1. 是所有 Partition 积压，还是少数 Partition 积压",[144,7128,7131],{"className":7129,"code":7130,"language":149,"meta":150},[147],"所有 Partition 的 Lag 都增长\n→ 更可能是整体消费能力不足、消费者异常或下游故障\n\n只有少数 Partition 的 Lag 很高\n→ 更可能是 Key 倾斜、热点业务或个别异常消息\n",[22,7132,7130],{"__ignoreMap":150},[18,7134,7135],{},"不能只看 Topic 的总 Lag。总数会掩盖单个热点 Partition，应同时查看每个 Partition 的 Lag、生产速率和消费速率。",[473,7137,7139],{"id":7138},"_2-消费者是否正常工作","2. 消费者是否正常工作",[18,7141,7142],{},"先检查：",[159,7144,7145,7148,7151,7154,7160,7163],{},[162,7146,7147],{},"消费实例是否存活，是否频繁重启。",[162,7149,7150],{},"Consumer Group 中是否存在空闲或反复加入、退出的成员。",[162,7152,7153],{},"是否频繁发生重平衡。",[162,7155,7156,7159],{},[22,7157,7158],{},"poll()"," 是否长时间没有被调用。",[162,7161,7162],{},"Offset 是否持续推进。",[162,7164,7165],{},"消费日志中是否存在反序列化、鉴权、提交 Offset 或业务异常。",[18,7167,7168,7169,7172],{},"如果业务处理时间超过 ",[22,7170,7171],{},"max.poll.interval.ms","，消费者会被认为没有正常推进消费，Partition 可能被重新分配。消费者不断处理、超时、重平衡，又可能造成更严重的积压。",[473,7174,7176],{"id":7175},"_3-单条消息处理是否变慢","3. 单条消息处理是否变慢",[18,7178,7179],{},"Kafka 消费本身往往不是最慢的部分，真正瓶颈通常在业务逻辑：",[159,7181,7182,7185,7188,7191,7194,7197],{},[162,7183,7184],{},"数据库慢 SQL、锁等待或连接池耗尽。",[162,7186,7187],{},"调用外部 RPC 超时。",[162,7189,7190],{},"每条消息单独写库，缺少批处理。",[162,7192,7193],{},"序列化、解压或大对象处理消耗 CPU。",[162,7195,7196],{},"消费逻辑加了粒度过大的锁。",[162,7198,7199],{},"下游 Elasticsearch、Redis 或第三方接口限流。",[18,7201,7202],{},"应把消费耗时拆分为：",[144,7204,7207],{"className":7205,"code":7206,"language":149,"meta":150},[147],"拉取耗时\n+ 反序列化耗时\n+ 业务计算耗时\n+ 数据库 \u002F RPC \u002F 下游写入耗时\n+ Offset 提交耗时\n",[22,7208,7206],{"__ignoreMap":150},[18,7210,7211],{},"只有找到主要耗时，优化才有方向。",[473,7213,7215],{"id":7214},"_4-是否被毒消息阻塞","4. 是否被“毒消息”阻塞",[18,7217,7218],{},"毒消息是指某条数据因为格式错误、业务状态异常或下游约束问题，每次消费都会失败。",[18,7220,7221],{},"如果代码采用无限重试：",[144,7223,7226],{"className":7224,"code":7225,"language":149,"meta":150},[147],"消费失败\n→ 立即重试\n→ 再次失败\n→ 一直占住当前 Partition\n",[22,7227,7225],{"__ignoreMap":150},[18,7229,7230],{},"该 Partition 后续所有消息都可能无法继续处理，表现为单分区 Lag 持续增大。",[473,7232,7234],{"id":7233},"_5-broker-或基础设施是否成为瓶颈","5. Broker 或基础设施是否成为瓶颈",[18,7236,7237],{},"还要检查：",[159,7239,7240,7243,7246,7249,7252],{},[162,7241,7242],{},"Broker 磁盘延迟、网络带宽和 CPU。",[162,7244,7245],{},"Fetch 请求延迟和错误率。",[162,7247,7248],{},"ISR 收缩、离线分区和副本同步异常。",[162,7250,7251],{},"消费者所在机器的 CPU、内存、GC 和网络。",[162,7253,7254],{},"是否发生跨机房访问或网络抖动。",[18,7256,7257],{},"如果 Broker 或网络已经饱和，单纯增加 Consumer 可能只会制造更多请求和竞争。",[14,7259,7261],{"id":7260},"三紧急处理先止住-lag-增长","三、紧急处理：先止住 Lag 增长",[473,7263,7265],{"id":7264},"_1-恢复异常消费者和下游","1. 恢复异常消费者和下游",[18,7267,7268],{},"如果积压源于实例宕机、数据库故障或下游超时，应优先恢复根因：",[144,7270,7273],{"className":7271,"code":7272,"language":149,"meta":150},[147],"恢复消费实例\n→ 恢复数据库 \u002F RPC \u002F 下游\n→ 确认 Offset 开始推进\n→ 再评估是否需要扩容\n",[22,7274,7272],{"__ignoreMap":150},[18,7276,7277],{},"下游仍不可用时强行增加消费者，只会放大连接数、重试和超时流量。",[473,7279,7281],{"id":7280},"_2-对非核心生产流量限流或降级","2. 对非核心生产流量限流或降级",[18,7283,7284],{},"如果生产速度仍在持续上升，而消费端已经满负荷，可以在业务允许时：",[159,7286,7287,7290,7293,7296],{},[162,7288,7289],{},"限制非核心事件的生产速率。",[162,7291,7292],{},"暂停可延迟的定时任务和批量导入。",[162,7294,7295],{},"合并低价值事件。",[162,7297,7298],{},"对非核心功能降级。",[18,7300,7301],{},"这不是最终解决方案，但能降低流入速度，给消费端争取恢复窗口。",[18,7303,7304],{},"这里的“合并低价值事件”，不是随意删除消息，而是对业务允许只保留最终状态或聚合结果的事件进行压缩。例如：",[144,7306,7309],{"className":7307,"code":7308,"language":149,"meta":150},[147],"同一个商品在 1 秒内连续产生 20 次缓存刷新通知\n→ 只保留一次刷新通知\n\n同一设备连续上报多次进度：10%、20%、30%\n→ 如果业务只关心最新进度，可以只发送 30%\n\n大量指标增量事件\n→ 在生产端按时间窗口汇总后，发送一条聚合结果\n",[22,7310,7308],{"__ignoreMap":150},[18,7312,7313],{},"合并前必须先定义清楚业务语义，包括按什么 Key 合并、合并时间窗口多长、保留最新值还是累计值，以及异常时如何补偿。订单状态流转、支付、账户流水、库存扣减、审计日志等每条事件都具有独立业务意义，不能使用这种方式合并。",[473,7315,7317],{"id":7316},"_3-确认消息保留时间是否足够","3. 确认消息保留时间是否足够",[18,7319,7320],{},"积压消息依赖 Kafka 的日志保留策略。如果预计追赶需要数小时或数天，应确认 Topic 的保留时间和磁盘空间是否足够。",[18,7322,7323],{},"如果最老未消费消息超过保留期限，旧日志可能被清理。此时即使消费者恢复，也无法从 Kafka 读取已经删除的数据。",[18,7325,7326],{},"紧急情况下可以评估临时延长保留时间，但必须同步评估 Broker 磁盘容量，避免积压尚未解决又触发磁盘告警。",[14,7328,7330],{"id":7329},"四提升消费能力","四、提升消费能力",[473,7332,7334],{"id":7333},"_1-增加-consumer-实例","1. 增加 Consumer 实例",[18,7336,7337],{},"增加同一 Consumer Group 中的实例，可以让更多 Partition 并行消费。",[18,7339,7340],{},"但有效并行度受 Partition 数量限制：",[144,7342,7345],{"className":7343,"code":7344,"language":149,"meta":150},[147],"8 个 Partition + 4 个 Consumer\n→ 可以继续扩容\n\n8 个 Partition + 8 个 Consumer\n→ 已接近分区级并行上限\n\n8 个 Partition + 12 个 Consumer\n→ 约 4 个 Consumer 无 Partition 可处理\n",[22,7346,7344],{"__ignoreMap":150},[18,7348,7349],{},"因此扩容前要同时查看当前 Partition 数量、消费者数量和分配情况。",[473,7351,7353],{"id":7352},"_2-优化单实例处理吞吐","2. 优化单实例处理吞吐",[18,7355,7356],{},"常见优化方式：",[159,7358,7359,7362,7365,7368,7371,7374,7377],{},[162,7360,7361],{},"把逐条写数据库改为批量写入。",[162,7363,7364],{},"合并相同业务 Key 的重复更新。",[162,7366,7367],{},"优化 SQL、索引和事务范围。",[162,7369,7370],{},"减少消费线程中的同步远程调用。",[162,7372,7373],{},"对可并行步骤使用有界线程池。",[162,7375,7376],{},"复用连接和客户端，避免每条消息重复创建。",[162,7378,7379],{},"降低无必要的日志和对象序列化开销。",[18,7381,686],{},[144,7383,7386],{"className":7384,"code":7385,"language":149,"meta":150},[147],"原方案：\n每条消息 → 一次数据库事务\n\n优化后：\n一批消息 → 分组校验 → 批量写入 → 提交对应 Offset\n",[22,7387,7385],{"__ignoreMap":150},[18,7389,7390,7391,33],{},"批量越大不一定越好。批次过大会增加内存占用、事务时间和失败重试成本，还可能让一次处理时间超过 ",[22,7392,7171],{},[18,7394,7395],{},"这里的“同步远程调用”，是指消费线程发送 RPC 或 HTTP 请求后，必须等待对方返回才能继续处理下一条消息：",[144,7397,7400],{"className":7398,"code":7399,"language":149,"meta":150},[147],"消费一条消息\n→ 调用远程库存服务\n→ 阻塞等待 50 ms\n→ 收到结果\n→ 再处理下一条消息\n",[22,7401,7399],{"__ignoreMap":150},[18,7403,7404],{},"即使本地计算只需要 1 ms，线程的大部分时间也消耗在网络等待上。如果每条消息都串行调用一次远程服务，单线程理论吞吐很容易被远程调用耗时限制。",[18,7406,7407],{},"“减少同步 RPC”可以从以下方向处理：",[159,7409,7410,7413,7416,7419,7422],{},[162,7411,7412],{},"把逐条调用改为批量接口，例如一次查询或提交 100 条。",[162,7414,7415],{},"对可复用且允许短时间不一致的数据使用本地缓存，减少重复查询。",[162,7417,7418],{},"把互不依赖的调用改成受控并发，但必须限制并发数，避免压垮下游。",[162,7420,7421],{},"将通知、埋点等非核心副作用发送到下游 Topic，由独立消费者异步执行。",[162,7423,7424],{},"合理设置超时、熔断和连接池，避免故障调用长期占住消费线程。",[18,7426,7427],{},"如果必须拿到远程结果才能保证当前消息处理正确，就不能为了吞吐简单删除该 RPC。此时更重要的是批量化、限制并发、幂等和下游容量治理。",[473,7429,7431],{"id":7430},"_3-谨慎调整消费参数","3. 谨慎调整消费参数",[18,7433,7434],{},"常见参数包括：",[144,7436,7438],{"className":4755,"code":7437,"language":4757,"meta":150,"style":150},"max.poll.records=500\nmax.poll.interval.ms=300000\nfetch.min.bytes=1\nfetch.max.wait.ms=500\nmax.partition.fetch.bytes=1048576\n",[22,7439,7440,7447,7454,7460,7466],{"__ignoreMap":150},[203,7441,7442,7445],{"class":205,"line":206},[203,7443,7444],{"class":209},"max.poll.records",[203,7446,6391],{"class":219},[203,7448,7449,7451],{"class":205,"line":251},[203,7450,7171],{"class":209},[203,7452,7453],{"class":219},"=300000\n",[203,7455,7456,7458],{"class":205,"line":260},[203,7457,6380],{"class":209},[203,7459,6383],{"class":219},[203,7461,7462,7464],{"class":205,"line":278},[203,7463,6388],{"class":209},[203,7465,6391],{"class":219},[203,7467,7468,7471],{"class":205,"line":285},[203,7469,7470],{"class":209},"max.partition.fetch.bytes",[203,7472,7473],{"class":219},"=1048576\n",[18,7475,7476],{},"调整原则：",[159,7478,7479,7488,7494,7499],{},[162,7480,7481,7482,7484,7485,7487],{},"如果一批消息处理不完，应适当减小 ",[22,7483,7444],{},"，或优化处理速度，而不是只把 ",[22,7486,7171],{}," 调得无限大。",[162,7489,7490,7491,7493],{},"增大 ",[22,7492,6380],{}," 可以提高批量拉取效率，但会增加低流量时的等待延迟。",[162,7495,7496,7498],{},[22,7497,7470],{}," 过小可能限制大批次拉取，但调大后要评估客户端内存。",[162,7500,7501],{},"参数优化只能降低 Kafka 拉取开销，无法解决数据库或 RPC 本身处理缓慢。",[473,7503,7505],{"id":7504},"_4-异步处理必须有边界","4. 异步处理必须有边界",[18,7507,7508],{},"一种常见设计是由 Poll 线程负责拉取，再把消息交给工作线程池处理：",[144,7510,7513],{"className":7511,"code":7512,"language":149,"meta":150},[147],"Poll 线程\n→ 有界队列\n→ 工作线程池\n→ 处理完成\n→ 按 Partition 推进可安全提交的 Offset\n",[22,7514,7512],{"__ignoreMap":150},[18,7516,7517],{},"为什么必须使用有界队列？假设 Kafka 已经积压 100 万条，Poll 线程仍然高速拉取并塞入无界队列，而工作线程每秒只能处理 100 条，那么积压只是从 Kafka 的磁盘日志转移到了 JVM 堆内存。结果可能是：",[144,7519,7522],{"className":7520,"code":7521,"language":149,"meta":150},[147],"队列持续增长\n→ 堆内存占用上升\n→ Full GC 频繁\n→ 消费进程响应变慢甚至 OOM\n→ 未提交的内存任务随进程退出而丢失并被重新投递\n",[22,7523,7521],{"__ignoreMap":150},[18,7525,7526],{},"Kafka 本身就是持久化队列，没有必要把尚未具备处理能力的消息提前搬进易失的 JVM 内存。",[6462,7528,7530,7531,40,7534],{"id":7529},"使用高低水位控制-pause-和-resume","使用高、低水位控制 ",[22,7532,7533],{},"pause()",[22,7535,7536],{},"resume()",[18,7538,7539],{},"可以为队列设置容量和两个阈值。例如队列容量为 10000：",[144,7541,7544],{"className":7542,"code":7543,"language":149,"meta":150},[147],"高水位：8000\n队列达到高水位\n→ Poll 线程对相关 Partition 调用 pause()\n→ 后续 poll() 暂停返回这些 Partition 的新消息\n\n低水位：3000\n工作线程处理后，队列下降到低水位\n→ Poll 线程调用 resume()\n→ 后续 poll() 重新拉取这些 Partition\n",[22,7545,7543],{"__ignoreMap":150},[18,7547,7548],{},"使用两个不同阈值是为了避免队列在一个临界值附近反复暂停、恢复。阈值不能照搬固定数字，应根据单条消息大小、堆内存、工作线程数和峰值处理耗时进行容量评估。",[18,7550,7551,7553,7554,7556],{},[22,7552,7533],{}," 的含义是暂停指定 Partition 在后续 ",[22,7555,7158],{}," 中返回新记录，它不会：",[159,7558,7559,7562,7565,7568],{},[162,7560,7561],{},"取消当前 Partition 的分配。",[162,7563,7564],{},"自动提交 Offset。",[162,7566,7567],{},"主动触发 Consumer Group 重平衡。",[162,7569,7570],{},"清除已经拉取并放入本地队列的消息。",[18,7572,7573,7574,7576,7577,7579],{},"暂停期间仍然必须持续调用 ",[22,7575,7158],{},"，让 Consumer 处理心跳、协调和重平衡事件。不能因为所有分区都已暂停，就让 Poll 线程长时间睡眠或等待队列清空，否则仍可能超过 ",[22,7578,7171],{}," 并失去分区。",[6462,7581,7583],{"id":7582},"只能由-consumer-所在线程控制拉取","只能由 Consumer 所在线程控制拉取",[18,7585,7586,7589,7590,53,7592,53,7594,7596],{},[22,7587,7588],{},"KafkaConsumer"," 不是线程安全的。通常由唯一的 Poll 线程持有并调用 ",[22,7591,7158],{},[22,7593,7533],{},[22,7595,7536],{}," 和提交 Offset；工作线程只处理业务，并把“完成结果”写回一个线程安全的完成队列。工作线程不能直接操作同一个 Consumer。",[18,7598,7599],{},"伪代码可以理解为：",[144,7601,7604],{"className":7602,"code":7603,"language":149,"meta":150},[147],"Poll 线程循环：\n    读取工作线程上报的完成结果\n    推进每个 Partition 的连续成功 Offset\n    根据队列水位执行 pause \u002F resume\n    poll() 拉取允许继续消费的 Partition\n    把记录投递到对应的有界工作队列\n\n工作线程：\n    处理业务\n    成功或失败结果写回完成队列\n    不直接调用 KafkaConsumer\n",[22,7605,7603],{"__ignoreMap":150},[6462,7607,7609],{"id":7608},"为什么要按-partition-提交连续成功区间","为什么要按 Partition 提交“连续成功区间”",[18,7611,7612],{},"假设同一 Partition 拉取到：",[144,7614,7617],{"className":7615,"code":7616,"language":149,"meta":150},[147],"Offset 100、101、102\n",[22,7618,7616],{"__ignoreMap":150},[18,7620,7621],{},"并发处理结果是：",[144,7623,7626],{"className":7624,"code":7625,"language":149,"meta":150},[147],"100 成功\n101 仍在处理\n102 成功\n",[22,7627,7625],{"__ignoreMap":150},[18,7629,7630],{},"此时最多只能提交 101，也就是声明 100 已处理完成；不能提交 103。否则进程在 101 完成前崩溃，重启后会从 103 开始，101 就可能被永久跳过。",[18,7632,7633],{},"只有 101 也成功后，100～102 形成连续成功区间，才能提交 103。若业务要求同一 Partition 严格有序，最简单的方式是同一 Partition 串行处理，不要并发执行。",[6462,7635,7636],{"id":7636},"重平衡时还要处理本地未完成任务",[18,7638,7639],{},"发生 Partition 撤销时，应停止向被撤销 Partition 的队列继续投递，并在回调允许的时间内提交已经连续处理成功的 Offset。尚未完成或来不及安全提交的任务，应允许新 Consumer 从旧 Offset 重新消费，因此业务处理必须具备幂等性。",[18,7641,7642],{},"需要共同遵守的约束包括：",[159,7644,7645,7650,7653,7660,7665,7668,7671],{},[162,7646,7647,7649],{},[22,7648,7588],{}," 本身不是线程安全的，不能让多个工作线程同时操作同一个 Consumer。",[162,7651,7652],{},"队列必须有界，不能把 Kafka 的积压无上限转移到 JVM 内存。",[162,7654,7655,6440,7657,7659],{},[22,7656,7533],{},[22,7658,7536],{}," 应由 Poll 线程执行，工作线程通过控制信号提出请求。",[162,7661,7662,7663,33],{},"即使 Partition 已暂停，Poll 线程也要持续执行 ",[22,7664,7158],{},[162,7666,7667],{},"Offset 必须在消息真正处理成功后提交。",[162,7669,7670],{},"同一 Partition 如果要求严格顺序，不能让后面的消息先于前面的消息产生业务效果。",[162,7672,7673],{},"并发完成后只能提交“连续成功区间”的下一个 Offset，不能跨过尚未完成或失败的消息。",[18,7675,7676],{},"异步化能提高吞吐，但会显著增加 Offset、顺序和失败处理的复杂度。",[14,7678,7680],{"id":7679},"五partition-不够时怎么办","五、Partition 不够时怎么办",[473,7682,7684],{"id":7683},"_1-增加-partition-主要提升未来并行度","1. 增加 Partition 主要提升未来并行度",[18,7686,7687],{},"如果 Consumer 数量已经等于 Partition 数量，且单分区处理能力也接近上限，可以评估增加 Partition。",[18,7689,7690],{},"但要明确：",[105,7692,7693],{},[18,7694,7695],{},[110,7696,7697],{},"给 Topic 新增 Partition，不会把旧 Partition 中已经积压的消息自动重新分布到新 Partition。",[18,7699,7700],{},"新 Partition 主要承接之后产生的新消息。旧积压仍然留在原来的 Partition 中，仍需由对应消费者追赶。",[473,7702,7704],{"id":7703},"_2-增加-partition-可能改变-key-路由","2. 增加 Partition 可能改变 Key 路由",[18,7706,7707],{},"默认的 Key 哈希结果通常与 Partition 数量有关。增加 Partition 后，同一个 Key 的新消息可能进入不同于历史消息的 Partition，从而影响跨扩容时点的顺序假设。",[18,7709,7710],{},"例如为了便于理解，假设某个分区规则可以简化为：",[144,7712,7715],{"className":7713,"code":7714,"language":149,"meta":150},[147],"目标分区 = hash(key) % Partition 数量\n",[22,7716,7714],{"__ignoreMap":150},[18,7718,7719],{},"同一个 Key 的哈希值为 13：",[144,7721,7724],{"className":7722,"code":7723,"language":149,"meta":150},[147],"原来 4 个 Partition：13 % 4 = 1，写入 P1\n扩容到 8 个 Partition：13 % 8 = 5，新消息写入 P5\n",[22,7725,7723],{"__ignoreMap":150},[18,7727,7728],{},"扩容前已经写入 P1 的旧消息不会被搬到 P5。P1 和 P5 又可能由不同 Consumer 并行处理，因此 P5 中的新消息可能先于 P1 中的旧消息完成。这个风险不只是“扩容瞬间”存在，而是会持续到旧分区中的相关历史消息全部处理完毕。",[18,7730,7731],{},"所谓“消费端能否接受跨分区处理”，实际是在问：",[105,7733,7734],{},[18,7735,7736],{},"同一个业务 Key 的旧消息和新消息同时位于不同 Partition，并可能并行、乱序完成时，业务是否仍然正确？",[18,7738,7739],{},"如果事件是独立日志或幂等的最终状态覆盖，业务可能可以接受；如果事件依赖严格顺序，例如“创建订单 → 支付成功 → 关闭订单”，就不能直接接受。",[18,7741,7742],{},"如果业务依赖同一 Key 的严格顺序，常见选择有：",[6462,7744,7746],{"id":7745},"方案一排空旧消息后再扩分区","方案一：排空旧消息后再扩分区",[144,7748,7751],{"className":7749,"code":7750,"language":149,"meta":150},[147],"限制或暂停生产\n→ 等待旧 Topic 的相关消息处理完成\n→ 增加 Partition\n→ 恢复生产\n",[22,7752,7750],{"__ignoreMap":150},[18,7754,7755],{},"这种方式逻辑最清楚，但需要维护窗口，而且在高流量系统中可能很难完全停写。",[6462,7757,7759],{"id":7758},"方案二新建-topic-并进行受控切换","方案二：新建 Topic 并进行受控切换",[18,7761,7762,7763,431],{},"例如创建具有目标分区数和新路由规则的 ",[22,7764,7765],{},"order-events-v2",[144,7767,7770],{"className":7768,"code":7769,"language":149,"meta":150},[147],"1. 创建并验证新 Topic\n2. 定义明确的切换时间、版本号或业务水位\n3. 新消息从切换点开始写入 v2\n4. v1 消费者继续处理切换点以前的旧消息\n5. 确认某个 Key 或全部旧消息越过安全水位后，再允许 v2 对应消息生效\n6. 完成核对后停止 v1，并保留回滚方案\n",[22,7771,7769],{"__ignoreMap":150},[18,7773,7774],{},"新建 Topic 的价值是把新旧路由、配置和消费组隔离开，便于验证与回滚；代价是必须设计双 Topic 切换、重复消息、遗漏检查和新旧 Offset 衔接。它不是 Kafka 自动迁移，而是业务自己完成的版本化切换。",[18,7776,7777],{},"因此扩分区前应明确回答：",[159,7779,7780,7783,7786,7789],{},[162,7781,7782],{},"分区器如何计算目标 Partition。",[162,7784,7785],{},"旧消息是否已经处理完成。",[162,7787,7788],{},"同一 Key 跨新旧 Partition 并行处理是否会破坏业务顺序。",[162,7790,7791],{},"是选择维护窗口排空旧消息，还是新建 Topic 做受控切换。",[473,7793,7795],{"id":7794},"_3-极端情况下建立临时加速链路","3. 极端情况下建立临时加速链路",[18,7797,7798],{},"当原 Topic 分区数严重不足、积压量巨大时，可以设计经过评审的临时方案：",[144,7800,7803],{"className":7801,"code":7802,"language":149,"meta":150},[147],"原 Topic\n→ 临时搬运程序\n→ 更高分区数的中转 Topic\n→ 临时扩容后的消费组\n→ 业务处理\n",[22,7804,7802],{"__ignoreMap":150},[18,7806,7807],{},"这会引入顺序、重复、Offset 衔接和回切问题，只适合经过完整设计和演练的场景，不能在生产事故中临时拍脑袋执行。",[14,7809,7811],{"id":7810},"六热点-partition-如何处理","六、热点 Partition 如何处理",[18,7813,7814],{},"如果只有少数 Partition 积压，需要检查消息 Key 分布。",[18,7816,7817],{},"例如所有消息都使用同一个固定 Key：",[144,7819,7822],{"className":7820,"code":7821,"language":149,"meta":150},[147],"key = \"default\"\n→ 所有消息进入同一 Partition\n→ 其他 Partition 空闲\n→ 单分区成为瓶颈\n",[22,7823,7821],{"__ignoreMap":150},[18,7825,7826],{},"解决方向包括：",[159,7828,7829,7832,7835,7841,7844],{},[162,7830,7831],{},"选择分布更均匀的业务 Key。",[162,7833,7834],{},"对非顺序场景取消固定 Key。",[162,7836,7837,7838,33],{},"对热点业务 Key 做可控分片，例如 ",[22,7839,7840],{},"userId + shardNo",[162,7842,7843],{},"单独拆分热点租户或热点业务到独立 Topic。",[162,7845,7846],{},"优化该 Partition 对应消费者的单条处理耗时。",[18,7848,7849],{},"Key 拆分会改变顺序语义。原来同一 Key 的全局顺序，拆分后通常只能保证每个子 Key 内部有序。",[14,7851,7853],{"id":7852},"七毒消息和重试如何处理","七、毒消息和重试如何处理",[18,7855,7856],{},"不要让一条永远失败的消息无限阻塞 Partition。常见处理策略是：",[144,7858,7861],{"className":7859,"code":7860,"language":149,"meta":150},[147],"第一次失败\n→ 记录错误并有限重试\n→ 仍然失败\n→ 进入重试 Topic \u002F 死信 Topic\n→ 主消费链路继续\n→ 告警和人工补偿\n",[22,7862,7860],{"__ignoreMap":150},[18,7864,7865],{},"需要保留：",[159,7867,7868,7871,7874,7877],{},[162,7869,7870],{},"原始 Topic、Partition 和 Offset。",[162,7872,7873],{},"消息 Key、事件 ID 和原始内容。",[162,7875,7876],{},"异常原因和重试次数。",[162,7878,7879],{},"首次失败和最后失败时间。",[18,7881,7882],{},"如果业务要求同一 Key 严格顺序，就不能简单跳过失败消息继续处理后续消息。此时应暂停对应 Partition，优先修复或补偿该消息；这是吞吐与顺序之间的业务取舍。",[14,7884,7886],{"id":7885},"八如何估算多久能消化完积压","八、如何估算多久能消化完积压",[18,7888,7889],{},"设：",[144,7891,7894],{"className":7892,"code":7893,"language":149,"meta":150},[147],"B = 当前积压消息数\nP = 当前生产速度（条\u002F秒）\nC = 扩容后的稳定消费速度（条\u002F秒）\n",[22,7895,7893],{"__ignoreMap":150},[18,7897,7898],{},"只有：",[144,7900,7903],{"className":7901,"code":7902,"language":149,"meta":150},[147],"C > P\n",[22,7904,7902],{"__ignoreMap":150},[18,7906,7907],{},"积压才会下降。理论恢复时间约为：",[144,7909,7912],{"className":7910,"code":7911,"language":149,"meta":150},[147],"恢复时间 T = B \u002F (C - P)\n",[22,7913,7911],{"__ignoreMap":150},[18,7915,686],{},[144,7917,7920],{"className":7918,"code":7919,"language":149,"meta":150},[147],"当前积压 B = 360 万条\n生产速度 P = 2000 条\u002F秒\n消费速度 C = 5000 条\u002F秒\n\n净消化速度 = 5000 - 2000 = 3000 条\u002F秒\n理论恢复时间 = 3600000 \u002F 3000 = 1200 秒\n≈ 20 分钟\n",[22,7921,7919],{"__ignoreMap":150},[18,7923,7924],{},"实际还要为重平衡、失败重试、下游波动和流量峰值预留余量。",[18,7926,4262,7927,7930],{},[22,7928,7929],{},"C \u003C= P","，无论运行多久都追不上，必须降低生产速率或提高有效消费能力。",[14,7932,7934],{"id":7933},"九offset-跳到最新为什么不是常规方案","九、Offset 跳到最新为什么不是常规方案",[18,7936,7937],{},"把消费组 Offset 重置到最新位置，确实可以让 Lag 迅速变成 0，但其本质是：",[105,7939,7940],{},[18,7941,7942],{},[110,7943,7944],{},"主动放弃尚未消费的历史消息。",[18,7946,7947],{},"只有同时满足以下条件才可以考虑：",[159,7949,7950,7953,7956,7959,7962],{},[162,7951,7952],{},"业务明确确认旧消息可以丢弃。",[162,7954,7955],{},"已评估对账、状态和下游影响。",[162,7957,7958],{},"已保留必要的审计或补偿数据。",[162,7960,7961],{},"操作前进行了 Dry Run，并确认 Topic、Consumer Group 和目标 Offset。",[162,7963,7964],{},"操作期间停止相关消费者，避免 Offset 并发变化。",[18,7966,7967],{},"资金、订单、库存等关键链路不能把重置 Offset 当作普通清积压手段。",[473,7969,7971],{"id":7970},"什么是-dry-run","什么是 Dry Run",[18,7973,7974],{},"Dry Run 就是“只预演，不执行”。它先计算并展示每个 Partition 将从哪个 Offset 调整到哪个 Offset，供操作人员核对 Topic、消费组、分区范围和目标位置，但不真正修改已提交 Offset。",[18,7976,7977],{},"例如将消费组预演重置到最新位置：",[144,7979,7983],{"className":7980,"code":7981,"language":7982,"meta":150,"style":150},"language-bash shiki shiki-themes github-light github-dark","bin\u002Fkafka-consumer-groups.sh \\\n  --bootstrap-server kafka.example.com:9092 \\\n  --group order-consumer-group \\\n  --topic order-events \\\n  --reset-offsets \\\n  --to-latest\n","bash",[22,7984,7985,7993,8003,8013,8023,8030],{"__ignoreMap":150},[203,7986,7987,7990],{"class":205,"line":206},[203,7988,7989],{"class":215},"bin\u002Fkafka-consumer-groups.sh",[203,7991,7992],{"class":718}," \\\n",[203,7994,7995,7998,8001],{"class":205,"line":251},[203,7996,7997],{"class":718},"  --bootstrap-server",[203,7999,8000],{"class":223}," kafka.example.com:9092",[203,8002,7992],{"class":718},[203,8004,8005,8008,8011],{"class":205,"line":260},[203,8006,8007],{"class":718},"  --group",[203,8009,8010],{"class":223}," order-consumer-group",[203,8012,7992],{"class":718},[203,8014,8015,8018,8021],{"class":205,"line":278},[203,8016,8017],{"class":718},"  --topic",[203,8019,8020],{"class":223}," order-events",[203,8022,7992],{"class":718},[203,8024,8025,8028],{"class":205,"line":285},[203,8026,8027],{"class":718},"  --reset-offsets",[203,8029,7992],{"class":718},[203,8031,8032],{"class":205,"line":541},[203,8033,8034],{"class":718},"  --to-latest\n",[18,8036,8037,8038,8041],{},"Kafka 4.1 的消费组工具默认只展示重置计划；确认结果无误后，显式增加 ",[22,8039,8040],{},"--execute"," 才会真正执行：",[144,8043,8045],{"className":7980,"code":8044,"language":7982,"meta":150,"style":150},"bin\u002Fkafka-consumer-groups.sh \\\n  --bootstrap-server kafka.example.com:9092 \\\n  --group order-consumer-group \\\n  --topic order-events \\\n  --reset-offsets \\\n  --to-latest \\\n  --execute\n",[22,8046,8047,8053,8061,8069,8077,8083,8090],{"__ignoreMap":150},[203,8048,8049,8051],{"class":205,"line":206},[203,8050,7989],{"class":215},[203,8052,7992],{"class":718},[203,8054,8055,8057,8059],{"class":205,"line":251},[203,8056,7997],{"class":718},[203,8058,8000],{"class":223},[203,8060,7992],{"class":718},[203,8062,8063,8065,8067],{"class":205,"line":260},[203,8064,8007],{"class":718},[203,8066,8010],{"class":223},[203,8068,7992],{"class":718},[203,8070,8071,8073,8075],{"class":205,"line":278},[203,8072,8017],{"class":718},[203,8074,8020],{"class":223},[203,8076,7992],{"class":718},[203,8078,8079,8081],{"class":205,"line":285},[203,8080,8027],{"class":718},[203,8082,7992],{"class":718},[203,8084,8085,8088],{"class":205,"line":541},[203,8086,8087],{"class":718},"  --to-latest",[203,8089,7992],{"class":718},[203,8091,8092],{"class":205,"line":549},[203,8093,8094],{"class":718},"  --execute\n",[18,8096,8097],{},"不同 Kafka 版本的工具参数可能略有差异，生产操作前应以实际部署版本的命令帮助和官方文档为准。",[473,8099,8100],{"id":8100},"为什么操作期间要停止消费者",[18,8102,8103,8104,8106],{},"活动消费者会继续执行 ",[22,8105,7158],{},"、处理消息并提交 Offset。如果管理人员同时重置 Offset，就可能发生竞态：",[144,8108,8111],{"className":8109,"code":8110,"language":149,"meta":150},[147],"管理员把 Offset 从 1000 重置到 5000\n→ 仍在运行的消费者随后提交自己内存中的旧进度 1200\n→ 重置结果被旧提交覆盖\n",[22,8112,8110],{"__ignoreMap":150},[18,8114,8115],{},"反过来也可能是消费者刚处理到 1300，管理员基于稍早看到的状态执行重置，导致已处理消息重复消费或尚未处理消息被跳过。活动成员还可能触发重平衡，使操作时看到的分区归属和 Offset 继续变化。",[18,8117,8118],{},"因此安全流程应是：",[144,8120,8123],{"className":8121,"code":8122,"language":149,"meta":150},[147],"停止该消费组的所有消费者\n→ 确认消费组已无活动成员\n→ 记录操作前 Offset\n→ 执行 Dry Run 并双人核对\n→ 使用 --execute 修改 Offset\n→ 再次查询并确认结果\n→ 启动消费者并观察消费位置\n",[22,8124,8122],{"__ignoreMap":150},[18,8126,8127],{},"这不仅是避免并发覆盖。Kafka 官方的消费组 Offset 重置说明也明确要求先确保消费实例处于非活动状态。",[14,8129,8131],{"id":8130},"十恢复后要做什么","十、恢复后要做什么",[18,8133,8134],{},"积压恢复不代表问题结束，还应完成：",[2695,8136,8137,8140,8143,8146,8149,8152,8155],{},[162,8138,8139],{},"复盘根因和时间线。",[162,8141,8142],{},"补充总 Lag、最大分区 Lag、时间 Lag 和增长率告警。",[162,8144,8145],{},"建立生产速度、消费速度和预计恢复时间看板。",[162,8147,8148],{},"对热点 Partition、毒消息和下游耗时建立专项监控。",[162,8150,8151],{},"按峰值流量进行容量压测，保留消费冗余。",[162,8153,8154],{},"演练消费者扩容、下游故障、重平衡和死信补偿。",[162,8156,8157],{},"核对积压期间是否发生消息过期、重复处理或业务遗漏。",[14,8159,8161],{"id":8160},"十一常见误区","十一、常见误区",[473,8163,8165],{"id":8164},"误区一积压就直接增加-consumer","误区一：积压就直接增加 Consumer",[18,8167,8168],{},"如果 Partition 数量不足、多数 Consumer 已空闲，或者瓶颈实际在数据库，增加 Consumer 不会解决问题，反而可能加剧下游压力。",[473,8170,8172,8173,8175],{"id":8171},"误区二把-maxpollrecords-调大就能提升吞吐","误区二：把 ",[22,8174,7444],{}," 调大就能提升吞吐",[18,8177,8178],{},"拉取更多消息不等于处理更快。批次过大还可能导致处理超时、重平衡和更高的失败重试成本。",[473,8180,8182],{"id":8181},"误区三增加-partition-会重新分配旧积压","误区三：增加 Partition 会重新分配旧积压",[18,8184,8185],{},"新增 Partition 不会迁移旧 Partition 中已有的消息，只能为后续消息提供更多并行空间。",[473,8187,8189],{"id":8188},"误区四lag-等于业务延迟","误区四：Lag 等于业务延迟",[18,8191,8192,8193,8196],{},"Lag 是 Offset 数量差，只表示还落后多少条记录，不等于时间延迟。相同 Lag 下，消费者处理速度越快，处理完现有积压所需的时间越短；如果生产仍在继续，则应使用 ",[22,8194,8195],{},"Lag ÷（消费速度－生产速度）"," 估算 Lag 归零时间。",[18,8198,8199],{},"当前业务已经延迟多久，应通过最老未处理消息的时间戳判断。消息大小影响积压字节量、网络与反序列化成本，单条处理耗时影响消费速度。因此排查时应把 Lag 条数、最老消息年龄、积压字节量和实际处理吞吐分开观察，不能混成一个指标。",[473,8201,8203],{"id":8202},"误区五把-offset-跳到最新就是清理积压","误区五：把 Offset 跳到最新就是清理积压",[18,8205,8206],{},"这不是“处理了积压”，而是“丢弃了积压”。必须经过明确的业务授权和风险评估。",[14,8208,5803],{"id":5803},[159,8210,8211,8216,8221,8228],{},[162,8212,8213],{},[119,8214,5826],{"href":5824,"rel":8215},[2080],[162,8217,8218],{},[119,8219,5840],{"href":5838,"rel":8220},[2080],[162,8222,8223],{},[119,8224,8227],{"href":8225,"rel":8226},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Foperations\u002Fconsumer-rebalance-protocol\u002F",[2080],"Apache Kafka Consumer Rebalance Protocol",[162,8229,8230],{},[119,8231,8234],{"href":8232,"rel":8233},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Foperations\u002Fbasic-kafka-operations\u002F",[2080],"Apache Kafka Basic Operations",[14,8236,3596],{"id":3596},[159,8238,8239,8244],{},[162,8240,8241],{},[119,8242,5900],{"href":8243},"\u002Fwiki\u002Fjava\u002Fkafka-high-concurrency-performance-availability\u002F",[162,8245,8246],{},[119,8247,4594],{"href":6881},[2111,8249,8250],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}",{"title":150,"searchDepth":251,"depth":251,"links":8252},[8253,8254,8255,8256,8257,8264,8269,8275,8280,8281,8282,8283,8287,8288,8296,8297],{"id":16,"depth":251,"text":16},{"id":115,"depth":251,"text":115},{"id":132,"depth":251,"text":132},{"id":6994,"depth":251,"text":6995},{"id":7121,"depth":251,"text":7122,"children":8258},[8259,8260,8261,8262,8263],{"id":7125,"depth":260,"text":7126},{"id":7138,"depth":260,"text":7139},{"id":7175,"depth":260,"text":7176},{"id":7214,"depth":260,"text":7215},{"id":7233,"depth":260,"text":7234},{"id":7260,"depth":251,"text":7261,"children":8265},[8266,8267,8268],{"id":7264,"depth":260,"text":7265},{"id":7280,"depth":260,"text":7281},{"id":7316,"depth":260,"text":7317},{"id":7329,"depth":251,"text":7330,"children":8270},[8271,8272,8273,8274],{"id":7333,"depth":260,"text":7334},{"id":7352,"depth":260,"text":7353},{"id":7430,"depth":260,"text":7431},{"id":7504,"depth":260,"text":7505},{"id":7679,"depth":251,"text":7680,"children":8276},[8277,8278,8279],{"id":7683,"depth":260,"text":7684},{"id":7703,"depth":260,"text":7704},{"id":7794,"depth":260,"text":7795},{"id":7810,"depth":251,"text":7811},{"id":7852,"depth":251,"text":7853},{"id":7885,"depth":251,"text":7886},{"id":7933,"depth":251,"text":7934,"children":8284},[8285,8286],{"id":7970,"depth":260,"text":7971},{"id":8100,"depth":260,"text":8100},{"id":8130,"depth":251,"text":8131},{"id":8160,"depth":251,"text":8161,"children":8289},[8290,8291,8293,8294,8295],{"id":8164,"depth":260,"text":8165},{"id":8171,"depth":260,"text":8292},"误区二：把 max.poll.records 调大就能提升吞吐",{"id":8181,"depth":260,"text":8182},{"id":8188,"depth":260,"text":8189},{"id":8202,"depth":260,"text":8203},{"id":5803,"depth":251,"text":5803},{"id":3596,"depth":251,"text":3596},"wiki:java:kafka-message-backlog-handling","从 Consumer Lag 识别、瓶颈定位、限流止血、消费者扩容、批量处理、热点分区和毒消息治理等方面，系统讲解 Kafka 消息积压处理方案。",{},74,"\u002Fjava\u002Fkafka-message-backlog-handling",{"title":6887,"description":8299},"java\u002Fkafka-message-backlog-handling","NjJRIgQZ4iCH7tlxGsujjTJ3RJTGuEk0nuO11Ax1vw8",{"id":8307,"title":8308,"body":8309,"commentId":10053,"description":10054,"difficulty":3667,"draft":2148,"extension":2149,"meta":10055,"navigation":756,"order":10056,"path":10057,"section":2153,"seo":10058,"stem":10059,"updated":5896,"__hash__":10060},"java\u002Fjava\u002Fmysql-transaction-isolation-and-phantom-read.md","MySQL 的隔离级别都有哪些？如何避免幻读？",{"type":7,"value":8310,"toc":10004},[8311,8313,8316,8355,8358,8372,8375,8378,8383,8385,8394,8396,8400,8403,8408,8411,8414,8495,8497,8502,8506,8510,8513,8519,8525,8529,8532,8538,8541,8546,8550,8553,8559,8562,8565,8570,8573,8581,8585,8589,8592,8603,8606,8610,8613,8616,8622,8625,8631,8639,8643,8646,8652,8658,8661,8664,8668,8671,8677,8697,8700,8703,8707,8710,8714,8720,8758,8761,8772,8775,8779,8782,8823,8826,8829,8833,8836,8879,8886,8926,8929,8960,8963,8966,8971,8974,8978,8981,9029,9036,9058,9061,9064,9089,9095,9098,9103,9107,9111,9114,9116,9151,9157,9161,9164,9167,9173,9176,9182,9185,9189,9192,9198,9201,9207,9210,9216,9236,9239,9244,9247,9281,9294,9300,9303,9309,9312,9315,9339,9345,9348,9382,9386,9389,9392,9429,9432,9435,9438,9443,9445,9450,9453,9470,9474,9477,9520,9523,9526,9529,9543,9546,9569,9572,9576,9582,9585,9591,9594,9606,9609,9612,9616,9619,9627,9630,9638,9641,9655,9659,9663,9665,9679,9683,9685,9699,9703,9705,9719,9722,9742,9746,9749,9772,9775,9795,9798,9828,9831,9853,9856,9867,9870,9885,9896,9899,9907,9910,9912,9916,9919,9923,9926,9934,9938,9941,9945,9948,9952,9955,9959,9962,9964,10001],[14,8312,16],{"id":16},[18,8314,8315],{},"MySQL InnoDB 支持四种事务隔离级别：读未提交、读已提交、可重复读和串行化。",[159,8317,8318,8324,8330,8349],{},[162,8319,8320,8323],{},[110,8321,8322],{},"读未提交（READ UNCOMMITTED）","：可能发生脏读、不可重复读和幻读。",[162,8325,8326,8329],{},[110,8327,8328],{},"读已提交（READ COMMITTED）","：只能读取已经提交的数据，但每次普通查询都会创建新的 Read View，因此可能发生不可重复读和幻读。",[162,8331,8332,8335,8336,53,8339,53,8342,40,8345,8348],{},[110,8333,8334],{},"可重复读（REPEATABLE READ）","：MySQL InnoDB 的默认隔离级别。同一事务中的普通快照读复用第一次查询建立的 Read View，避免不可重复读，并通过一致性快照避免看到后来插入的幻影行；对于 ",[22,8337,8338],{},"SELECT ... FOR UPDATE",[22,8340,8341],{},"SELECT ... FOR SHARE",[22,8343,8344],{},"UPDATE",[22,8346,8347],{},"DELETE"," 等当前读，则通过 Next-Key Lock 锁住索引记录及其前面的间隙，阻止其他事务在查询范围内插入新记录。",[162,8350,8351,8354],{},[110,8352,8353],{},"串行化（SERIALIZABLE）","：隔离性最强，事务近似串行执行，可以避免脏读、不可重复读和幻读，但并发能力最低。",[18,8356,8357],{},"因此，回答“InnoDB 如何避免幻读”时，需要区分两类读取：",[2695,8359,8360,8367],{},[162,8361,8362,8363,8366],{},"普通 ",[22,8364,8365],{},"SELECT"," 属于快照读，REPEATABLE READ 通过 MVCC 和同一个 Read View，让事务始终读取同一份一致性快照。",[162,8368,8369,8371],{},[22,8370,8338],{}," 等属于当前读，REPEATABLE READ 通过 Gap Lock 和 Next-Key Lock 阻止其他事务向锁定范围插入新记录。",[18,8373,8374],{},"如果业务逻辑是“先查询是否存在，再决定是否插入或更新”，仅依赖快照读通常不够，应在 REPEATABLE READ 下使用合适索引和锁定读，并通过唯一索引等数据库约束作为最终兜底。",[473,8376,8377],{"id":8377},"一句话总结",[105,8379,8380],{},[18,8381,8382],{},"InnoDB 默认使用 REPEATABLE READ：普通查询靠 MVCC 的一致性快照避免看见幻影行，当前读靠 Next-Key Lock 锁住记录和间隙，阻止幻影行被插入。",[14,8384,115],{"id":115},[18,8386,8387],{},[119,8388,8391],{"href":8389,"target":122,"rel":8390},"\u002Fimages\u002Fwiki\u002Fjava\u002Fmysql-transaction-isolation-and-phantom-read.svg",[124,125],[127,8392],{"src":8389,"alt":8393},"MySQL 四种事务隔离级别与 InnoDB 避免幻读的机制",[14,8395,132],{"id":132},[14,8397,8399],{"id":8398},"一事务隔离级别解决什么问题","一、事务隔离级别解决什么问题",[18,8401,8402],{},"多个事务并发执行时，一个事务的写入可能影响另一个事务的读取。隔离级别用于规定：",[105,8404,8405],{},[18,8406,8407],{},"一个事务能够看到其他事务的哪些修改，以及什么时候能够看到这些修改。",[18,8409,8410],{},"隔离级别越高，事务之间的影响越小，但加锁和等待通常越多，并发能力也可能越低。",[18,8412,8413],{},"MySQL InnoDB 支持 SQL 标准定义的四种隔离级别：",[299,8415,8416,8436],{},[302,8417,8418],{},[305,8419,8420,8423,8427,8430,8433],{},[308,8421,8422],{},"隔离级别",[308,8424,8426],{"align":8425},"right","脏读",[308,8428,8429],{"align":8425},"不可重复读",[308,8431,8432],{"align":8425},"幻读",[308,8434,8435],{"align":8425},"并发能力",[315,8437,8438,8453,8467,8481],{},[305,8439,8440,8443,8446,8448,8450],{},[320,8441,8442],{},"READ UNCOMMITTED",[320,8444,8445],{"align":8425},"可能",[320,8447,8445],{"align":8425},[320,8449,8445],{"align":8425},[320,8451,8452],{"align":8425},"最高",[305,8454,8455,8458,8461,8463,8465],{},[320,8456,8457],{},"READ COMMITTED",[320,8459,8460],{"align":8425},"避免",[320,8462,8445],{"align":8425},[320,8464,8445],{"align":8425},[320,8466,3918],{"align":8425},[305,8468,8469,8472,8474,8476,8479],{},[320,8470,8471],{},"REPEATABLE READ",[320,8473,8460],{"align":8425},[320,8475,8460],{"align":8425},[320,8477,8478],{"align":8425},"InnoDB 通过 MVCC 和 Next-Key Lock 处理",[320,8480,3918],{"align":8425},[305,8482,8483,8486,8488,8490,8492],{},[320,8484,8485],{},"SERIALIZABLE",[320,8487,8460],{"align":8425},[320,8489,8460],{"align":8425},[320,8491,8460],{"align":8425},[320,8493,8494],{"align":8425},"最低",[18,8496,4831],{},[105,8498,8499],{},[18,8500,8501],{},"“REPEATABLE READ 可能发生幻读”是 SQL 标准层面的常见结论；MySQL InnoDB 在该级别上又实现了 MVCC 和 Next-Key Lock，对快照读和当前读分别处理幻读问题。",[14,8503,8505],{"id":8504},"二三种并发读取问题","二、三种并发读取问题",[473,8507,8509],{"id":8508},"_1-脏读","1. 脏读",[18,8511,8512],{},"事务 A 读取了事务 B 尚未提交的数据。如果事务 B 最后回滚，事务 A 读到的就是从未真正生效的数据。",[144,8514,8517],{"className":8515,"code":8516,"language":149,"meta":150},[147],"事务 A                         事务 B\n                              修改余额 100 → 0\n读取余额，得到 0\n                              ROLLBACK\n",[22,8518,8516],{"__ignoreMap":150},[18,8520,8521,8522,8524],{},"这里的 ",[22,8523,2412],{}," 就是脏数据。",[473,8526,8528],{"id":8527},"_2-不可重复读","2. 不可重复读",[18,8530,8531],{},"同一事务两次读取同一行，得到的字段值不同。",[144,8533,8536],{"className":8534,"code":8535,"language":149,"meta":150},[147],"事务 A                         事务 B\n读取 id=1，余额为 100\n                              修改余额为 80\n                              COMMIT\n再次读取 id=1，余额为 80\n",[22,8537,8535],{"__ignoreMap":150},[18,8539,8540],{},"不可重复读关注的是：",[105,8542,8543],{},[18,8544,8545],{},"同一行记录的内容发生了变化。",[473,8547,8549],{"id":8548},"_3-幻读","3. 幻读",[18,8551,8552],{},"同一事务使用相同查询条件执行两次查询，第二次返回的结果集合中出现了第一次不存在的记录，或者原有记录消失。",[144,8554,8557],{"className":8555,"code":8556,"language":149,"meta":150},[147],"事务 A                         事务 B\n查询 amount > 100，返回 3 行\n                              插入一条 amount=150 的记录\n                              COMMIT\n再次查询 amount > 100，返回 4 行\n",[22,8558,8556],{"__ignoreMap":150},[18,8560,8561],{},"新增的第 4 行就是幻影行。",[18,8563,8564],{},"幻读关注的是：",[105,8566,8567],{},[18,8568,8569],{},"满足查询条件的记录集合发生了变化。",[18,8571,8572],{},"所以，不可重复读和幻读的区别可以记为：",[159,8574,8575,8578],{},[162,8576,8577],{},"不可重复读：同一行的值变了。",[162,8579,8580],{},"幻读：符合条件的行数或记录集合变了。",[14,8582,8584],{"id":8583},"三四种隔离级别","三、四种隔离级别",[473,8586,8588],{"id":8587},"_1-read-uncommitted读未提交","1. READ UNCOMMITTED：读未提交",[18,8590,8591],{},"事务可以读取其他事务尚未提交的修改，因此可能发生：",[159,8593,8594,8597,8600],{},[162,8595,8596],{},"脏读；",[162,8598,8599],{},"不可重复读；",[162,8601,8602],{},"幻读。",[18,8604,8605],{},"它的隔离性最低，实际业务系统很少使用。",[473,8607,8609],{"id":8608},"_2-read-committed读已提交","2. READ COMMITTED：读已提交",[18,8611,8612],{},"事务只能读取已经提交的数据，因此不会发生脏读。",[18,8614,8615],{},"但在 InnoDB 中，每一次普通一致性读都会创建一个新的 Read View：",[144,8617,8620],{"className":8618,"code":8619,"language":149,"meta":150},[147],"第一次 SELECT → Read View 1\n第二次 SELECT → Read View 2\n第三次 SELECT → Read View 3\n",[22,8621,8619],{"__ignoreMap":150},[18,8623,8624],{},"如果其他事务在两次查询之间提交了修改，后一次查询就可能看到新数据，因此可能发生：",[159,8626,8627,8629],{},[162,8628,8599],{},[162,8630,8602],{},[18,8632,8633,8634,40,8636,8638],{},"另外，在 READ COMMITTED 下，锁定读、",[22,8635,8344],{},[22,8637,8347],{}," 通常只锁索引记录，不锁记录前面的间隙。除了外键约束检查和重复键检查等情况外，Gap Lock 被禁用，因此其他事务仍可能向间隙插入新记录。",[473,8640,8642],{"id":8641},"_3-repeatable-read可重复读","3. REPEATABLE READ：可重复读",[18,8644,8645],{},"这是 InnoDB 的默认隔离级别。",[18,8647,8648,8649,8651],{},"对于普通 ",[22,8650,8365],{},"，事务中的第一次一致性读会建立 Read View，之后的普通一致性读继续使用该快照：",[144,8653,8656],{"className":8654,"code":8655,"language":149,"meta":150},[147],"第一次普通 SELECT → 创建 Read View\n第二次普通 SELECT → 复用同一个 Read View\n第三次普通 SELECT → 复用同一个 Read View\n",[22,8657,8655],{"__ignoreMap":150},[18,8659,8660],{},"因此，其他事务之后提交的更新、删除或插入不会出现在当前事务的旧快照中。",[18,8662,8663],{},"对于锁定读和数据修改语句，InnoDB 会读取最新可用数据，并根据查询使用的索引范围加锁。范围条件通常会使用 Gap Lock 或 Next-Key Lock，阻止其他事务向被锁定的范围插入新记录。",[473,8665,8667],{"id":8666},"_4-serializable串行化","4. SERIALIZABLE：串行化",[18,8669,8670],{},"SERIALIZABLE 是隔离性最强的级别。",[18,8672,8673,8674,8676],{},"在关闭自动提交的情况下，InnoDB 会把普通 ",[22,8675,8365],{}," 隐式转换为共享锁定读，效果类似：",[144,8678,8682],{"className":8679,"code":8680,"language":8681,"meta":150,"style":150},"language-sql shiki shiki-themes github-light github-dark","SELECT ... FOR SHARE;\n","sql",[22,8683,8684],{"__ignoreMap":150},[203,8685,8686,8688,8691,8694],{"class":205,"line":206},[203,8687,8365],{"class":209},[203,8689,8690],{"class":219}," ... ",[203,8692,8693],{"class":209},"FOR",[203,8695,8696],{"class":219}," SHARE;\n",[18,8698,8699],{},"这样事务之间会产生更多锁等待，能够避免脏读、不可重复读和幻读，但吞吐量通常明显下降。",[18,8701,8702],{},"它适合一致性要求极高、并发量较低，或者确实需要事务串行执行的场景。",[14,8704,8706],{"id":8705},"四快照读与当前读","四、快照读与当前读",[18,8708,8709],{},"理解 InnoDB 如何处理幻读，必须先区分快照读和当前读。",[473,8711,8713],{"id":8712},"_1-快照读","1. 快照读",[18,8715,8716,8717,8719],{},"在 READ COMMITTED 和 REPEATABLE READ 下，普通 ",[22,8718,8365],{}," 通常属于快照读：",[144,8721,8723],{"className":8679,"code":8722,"language":8681,"meta":150,"style":150},"SELECT * FROM orders WHERE amount BETWEEN 100 AND 200;\n",[22,8724,8725],{"__ignoreMap":150},[203,8726,8727,8729,8732,8735,8738,8741,8744,8747,8750,8753,8756],{"class":205,"line":206},[203,8728,8365],{"class":209},[203,8730,8731],{"class":209}," *",[203,8733,8734],{"class":209}," FROM",[203,8736,8737],{"class":219}," orders ",[203,8739,8740],{"class":209},"WHERE",[203,8742,8743],{"class":219}," amount ",[203,8745,8746],{"class":209},"BETWEEN",[203,8748,8749],{"class":718}," 100",[203,8751,8752],{"class":209}," AND",[203,8754,8755],{"class":718}," 200",[203,8757,1297],{"class":219},[18,8759,8760],{},"快照读：",[159,8762,8763,8766,8769],{},[162,8764,8765],{},"不对查询记录加行锁；",[162,8767,8768],{},"通过 MVCC 读取某个时间点的一致性数据；",[162,8770,8771],{},"其他事务可以继续修改、删除或插入数据。",[18,8773,8774],{},"REPEATABLE READ 下，同一事务中的普通快照读复用同一个 Read View，所以后来提交的记录对当前快照不可见。",[473,8776,8778],{"id":8777},"_2-当前读","2. 当前读",[18,8780,8781],{},"下面这些操作需要读取最新可用的数据，并对数据加锁：",[144,8783,8785],{"className":8679,"code":8784,"language":8681,"meta":150,"style":150},"SELECT ... FOR UPDATE;\nSELECT ... FOR SHARE;\nUPDATE ...;\nDELETE ...;\n",[22,8786,8787,8800,8810,8817],{"__ignoreMap":150},[203,8788,8789,8791,8793,8795,8798],{"class":205,"line":206},[203,8790,8365],{"class":209},[203,8792,8690],{"class":219},[203,8794,8693],{"class":209},[203,8796,8797],{"class":209}," UPDATE",[203,8799,1297],{"class":219},[203,8801,8802,8804,8806,8808],{"class":205,"line":251},[203,8803,8365],{"class":209},[203,8805,8690],{"class":219},[203,8807,8693],{"class":209},[203,8809,8696],{"class":219},[203,8811,8812,8814],{"class":205,"line":260},[203,8813,8344],{"class":209},[203,8815,8816],{"class":219}," ...;\n",[203,8818,8819,8821],{"class":205,"line":278},[203,8820,8347],{"class":209},[203,8822,8816],{"class":219},[18,8824,8825],{},"它们通常被称为当前读或锁定读。",[18,8827,8828],{},"当前读不能只依靠旧快照，因为业务接下来可能要修改数据。它必须在最新数据状态上进行判断，并锁住相关记录或范围，避免判断完成后数据立即被并发事务改变。",[14,8830,8832],{"id":8831},"五快照读如何避免幻读","五、快照读如何避免幻读",[18,8834,8835],{},"假设当前使用 REPEATABLE READ：",[144,8837,8839],{"className":8679,"code":8838,"language":8681,"meta":150,"style":150},"START TRANSACTION;\n\nSELECT * FROM orders\nWHERE amount BETWEEN 100 AND 200;\n",[22,8840,8841,8848,8852,8863],{"__ignoreMap":150},[203,8842,8843,8846],{"class":205,"line":206},[203,8844,8845],{"class":209},"START TRANSACTION",[203,8847,1297],{"class":219},[203,8849,8850],{"class":205,"line":251},[203,8851,757],{"emptyLinePlaceholder":756},[203,8853,8854,8856,8858,8860],{"class":205,"line":260},[203,8855,8365],{"class":209},[203,8857,8731],{"class":209},[203,8859,8734],{"class":209},[203,8861,8862],{"class":219}," orders\n",[203,8864,8865,8867,8869,8871,8873,8875,8877],{"class":205,"line":278},[203,8866,8740],{"class":209},[203,8868,8743],{"class":219},[203,8870,8746],{"class":209},[203,8872,8749],{"class":718},[203,8874,8752],{"class":209},[203,8876,8755],{"class":718},[203,8878,1297],{"class":219},[18,8880,8881,8882,8885],{},"第一次普通查询建立 Read View。此后事务 B 插入一条 ",[22,8883,8884],{},"amount=150"," 的记录并提交：",[144,8887,8889],{"className":8679,"code":8888,"language":8681,"meta":150,"style":150},"INSERT INTO orders(user_id, amount)\nVALUES (10, 150);\n\nCOMMIT;\n",[22,8890,8891,8899,8915,8919],{"__ignoreMap":150},[203,8892,8893,8896],{"class":205,"line":206},[203,8894,8895],{"class":209},"INSERT INTO",[203,8897,8898],{"class":219}," orders(user_id, amount)\n",[203,8900,8901,8904,8906,8908,8910,8913],{"class":205,"line":251},[203,8902,8903],{"class":209},"VALUES",[203,8905,5163],{"class":219},[203,8907,2461],{"class":718},[203,8909,5179],{"class":219},[203,8911,8912],{"class":718},"150",[203,8914,227],{"class":219},[203,8916,8917],{"class":205,"line":260},[203,8918,757],{"emptyLinePlaceholder":756},[203,8920,8921,8924],{"class":205,"line":278},[203,8922,8923],{"class":209},"COMMIT",[203,8925,1297],{"class":219},[18,8927,8928],{},"事务 A 再次执行相同的普通查询：",[144,8930,8932],{"className":8679,"code":8931,"language":8681,"meta":150,"style":150},"SELECT * FROM orders\nWHERE amount BETWEEN 100 AND 200;\n",[22,8933,8934,8944],{"__ignoreMap":150},[203,8935,8936,8938,8940,8942],{"class":205,"line":206},[203,8937,8365],{"class":209},[203,8939,8731],{"class":209},[203,8941,8734],{"class":209},[203,8943,8862],{"class":219},[203,8945,8946,8948,8950,8952,8954,8956,8958],{"class":205,"line":251},[203,8947,8740],{"class":209},[203,8949,8743],{"class":219},[203,8951,8746],{"class":209},[203,8953,8749],{"class":718},[203,8955,8752],{"class":209},[203,8957,8755],{"class":718},[203,8959,1297],{"class":219},[18,8961,8962],{},"由于事务 A 仍然使用原来的 Read View，新插入的记录不在该快照的可见范围内，因此第二次查询不会看到它。",[18,8964,8965],{},"这里需要准确理解：",[105,8967,8968],{},[18,8969,8970],{},"MVCC 没有阻止事务 B 插入记录，只是让这条后来提交的记录对事务 A 的旧快照不可见。",[18,8972,8973],{},"因此，快照读解决的是“当前事务看到什么”，而不是“其他事务能不能写入”。",[14,8975,8977],{"id":8976},"六当前读如何避免幻读","六、当前读如何避免幻读",[18,8979,8980],{},"如果业务需要先锁定一个范围，再根据查询结果修改数据，可以使用：",[144,8982,8984],{"className":8679,"code":8983,"language":8681,"meta":150,"style":150},"START TRANSACTION;\n\nSELECT * FROM orders\nWHERE amount BETWEEN 100 AND 200\nFOR UPDATE;\n",[22,8985,8986,8992,8996,9006,9021],{"__ignoreMap":150},[203,8987,8988,8990],{"class":205,"line":206},[203,8989,8845],{"class":209},[203,8991,1297],{"class":219},[203,8993,8994],{"class":205,"line":251},[203,8995,757],{"emptyLinePlaceholder":756},[203,8997,8998,9000,9002,9004],{"class":205,"line":260},[203,8999,8365],{"class":209},[203,9001,8731],{"class":209},[203,9003,8734],{"class":209},[203,9005,8862],{"class":219},[203,9007,9008,9010,9012,9014,9016,9018],{"class":205,"line":278},[203,9009,8740],{"class":209},[203,9011,8743],{"class":219},[203,9013,8746],{"class":209},[203,9015,8749],{"class":718},[203,9017,8752],{"class":209},[203,9019,9020],{"class":718}," 200\n",[203,9022,9023,9025,9027],{"class":205,"line":285},[203,9024,8693],{"class":209},[203,9026,8797],{"class":209},[203,9028,1297],{"class":219},[18,9030,9031,9032,9035],{},"假设 ",[22,9033,9034],{},"amount"," 上存在普通索引：",[144,9037,9039],{"className":8679,"code":9038,"language":8681,"meta":150,"style":150},"CREATE INDEX idx_orders_amount ON orders(amount);\n",[22,9040,9041],{"__ignoreMap":150},[203,9042,9043,9046,9049,9052,9055],{"class":205,"line":206},[203,9044,9045],{"class":209},"CREATE",[203,9047,9048],{"class":209}," INDEX",[203,9050,9051],{"class":215}," idx_orders_amount",[203,9053,9054],{"class":209}," ON",[203,9056,9057],{"class":219}," orders(amount);\n",[18,9059,9060],{},"在 REPEATABLE READ 下，InnoDB 扫描这个索引范围时，通常会对索引记录及其间隙加 Next-Key Lock。",[18,9062,9063],{},"此时另一个事务尝试插入：",[144,9065,9067],{"className":8679,"code":9066,"language":8681,"meta":150,"style":150},"INSERT INTO orders(user_id, amount)\nVALUES (10, 150);\n",[22,9068,9069,9075],{"__ignoreMap":150},[203,9070,9071,9073],{"class":205,"line":206},[203,9072,8895],{"class":209},[203,9074,8898],{"class":219},[203,9076,9077,9079,9081,9083,9085,9087],{"class":205,"line":251},[203,9078,8903],{"class":209},[203,9080,5163],{"class":219},[203,9082,2461],{"class":718},[203,9084,5179],{"class":219},[203,9086,8912],{"class":718},[203,9088,227],{"class":219},[18,9090,9091,9092,9094],{},"由于 ",[22,9093,8912],{}," 落在已经锁定的索引范围内，该插入通常会被阻塞，直到前一个事务提交或回滚。",[18,9096,9097],{},"这次不是“让新记录不可见”，而是：",[105,9099,9100],{},[18,9101,9102],{},"阻止新记录进入已经锁定的查询范围。",[14,9104,9106],{"id":9105},"七record-lockgap-lock-与-next-key-lock","七、Record Lock、Gap Lock 与 Next-Key Lock",[473,9108,9110],{"id":9109},"_1-record-lock","1. Record Lock",[18,9112,9113],{},"Record Lock 锁住的是索引记录。",[18,9115,686],{},[144,9117,9119],{"className":8679,"code":9118,"language":8681,"meta":150,"style":150},"SELECT * FROM orders\nWHERE id = 100\nFOR UPDATE;\n",[22,9120,9121,9131,9143],{"__ignoreMap":150},[203,9122,9123,9125,9127,9129],{"class":205,"line":206},[203,9124,8365],{"class":209},[203,9126,8731],{"class":209},[203,9128,8734],{"class":209},[203,9130,8862],{"class":219},[203,9132,9133,9135,9138,9140],{"class":205,"line":251},[203,9134,8740],{"class":209},[203,9136,9137],{"class":219}," id ",[203,9139,495],{"class":209},[203,9141,9142],{"class":718}," 100\n",[203,9144,9145,9147,9149],{"class":205,"line":260},[203,9146,8693],{"class":209},[203,9148,8797],{"class":209},[203,9150,1297],{"class":219},[18,9152,4262,9153,9156],{},[22,9154,9155],{},"id"," 是唯一索引，并且使用唯一等值条件准确定位一条记录，InnoDB 通常只锁找到的索引记录，不需要锁前面的间隙。",[473,9158,9160],{"id":9159},"_2-gap-lock","2. Gap Lock",[18,9162,9163],{},"Gap Lock 锁住的是两个索引记录之间的间隙，而不是某一条已经存在的记录。",[18,9165,9166],{},"假设索引中已有：",[144,9168,9171],{"className":9169,"code":9170,"language":149,"meta":150},[147],"100\n200\n300\n",[22,9172,9170],{"__ignoreMap":150},[18,9174,9175],{},"那么其中存在：",[144,9177,9180],{"className":9178,"code":9179,"language":149,"meta":150},[147],"(-∞, 100)\n(100, 200)\n(200, 300)\n(300, +∞)\n",[22,9181,9179],{"__ignoreMap":150},[18,9183,9184],{},"Gap Lock 的主要作用是限制其他事务在对应间隙插入新索引记录。",[473,9186,9188],{"id":9187},"_3-next-key-lock","3. Next-Key Lock",[18,9190,9191],{},"Next-Key Lock 的准确定义是：",[144,9193,9196],{"className":9194,"code":9195,"language":149,"meta":150},[147],"Next-Key Lock = Record Lock + 该记录前面的 Gap Lock\n",[22,9197,9195],{"__ignoreMap":150},[18,9199,9200],{},"这里的“前面”是指索引排序中的前一个间隙。假设索引值为：",[144,9202,9205],{"className":9203,"code":9204,"language":149,"meta":150},[147],"3\n5\n8\n",[22,9206,9204],{"__ignoreMap":150},[18,9208,9209],{},"可能形成的 Next-Key Lock 区间是：",[144,9211,9214],{"className":9212,"code":9213,"language":149,"meta":150},[147],"(-∞, 3]\n(3, 5]\n(5, 8]\n(8, +∞)\n",[22,9215,9213],{"__ignoreMap":150},[18,9217,9218,9219,9222,9223,9226,9227,9229,9230,6440,9233,9235],{},"例如，对索引记录 ",[22,9220,9221],{},"5"," 加 Next-Key Lock，可以概念性地理解为锁住 ",[22,9224,9225],{},"(3, 5]","：既保护记录 ",[22,9228,9221],{},"，又阻止其他事务向 ",[22,9231,9232],{},"3",[22,9234,9221],{}," 之间插入新索引记录。",[18,9237,9238],{},"这里需要区分单个 Next-Key Lock 和一条 SQL 最终获得的整组锁：",[105,9240,9241],{},[18,9242,9243],{},"一个 Next-Key Lock 只包含当前记录和它前面的间隙，但一条 SQL 可能同时获得多个 Record Lock、Gap Lock 和 Next-Key Lock，最终覆盖查询记录前后的整个范围。",[18,9245,9246],{},"例如，在 REPEATABLE READ 下执行：",[144,9248,9250],{"className":8679,"code":9249,"language":8681,"meta":150,"style":150},"SELECT * FROM orders\nWHERE amount = 5\nFOR UPDATE;\n",[22,9251,9252,9262,9273],{"__ignoreMap":150},[203,9253,9254,9256,9258,9260],{"class":205,"line":206},[203,9255,8365],{"class":209},[203,9257,8731],{"class":209},[203,9259,8734],{"class":209},[203,9261,8862],{"class":219},[203,9263,9264,9266,9268,9270],{"class":205,"line":251},[203,9265,8740],{"class":209},[203,9267,8743],{"class":219},[203,9269,495],{"class":209},[203,9271,9272],{"class":718}," 5\n",[203,9274,9275,9277,9279],{"class":205,"line":260},[203,9276,8693],{"class":209},[203,9278,8797],{"class":209},[203,9280,1297],{"class":219},[18,9282,4262,9283,9285,9286,9289,9290,9293],{},[22,9284,9034],{}," 是普通非唯一索引，并且相邻索引值为 ",[22,9287,9288],{},"3、5、8","，为了防止其他事务再次插入 ",[22,9291,9292],{},"amount=5"," 的记录，最终需要保护：",[144,9295,9298],{"className":9296,"code":9297,"language":149,"meta":150},[147],"记录 5\n记录 5 前面的间隙：(3, 5)\n记录 5 后面的间隙：(5, 8)\n",[22,9299,9297],{"__ignoreMap":150},[18,9301,9302],{},"可以概念性地理解为：",[144,9304,9307],{"className":9305,"code":9306,"language":149,"meta":150},[147],"(3, 5]  → 记录 5 的 Next-Key Lock\n(5, 8)  → 查询范围结束位置的 Gap Lock\n",[22,9308,9306],{"__ignoreMap":150},[18,9310,9311],{},"所以，“等值查询可能锁住匹配记录及其前后间隙”是对一条 SQL 最终锁定范围的描述；它不代表单个 Next-Key Lock 同时包含记录前后两个间隙。",[18,9313,9314],{},"还要区分以下情况：",[159,9316,9317,9322,9333,9336],{},[162,9318,8362,9319,9321],{},[22,9320,8365],{}," 是快照读，不会因为这个查询条件加 Next-Key Lock。",[162,9323,9324,53,9326,53,9328,40,9330,9332],{},[22,9325,8338],{},[22,9327,8341],{},[22,9329,8344],{},[22,9331,8347],{}," 才属于锁定读或当前读。",[162,9334,9335],{},"如果使用唯一索引和唯一等值条件，并且准确找到已有记录，InnoDB 通常只加 Record Lock，不需要锁前后间隙。",[162,9337,9338],{},"如果查询条件没有命中唯一记录，或者使用普通非唯一索引，InnoDB 会根据实际扫描范围使用 Gap Lock 或 Next-Key Lock。",[18,9340,9341,9342,9344],{},"具体获得哪些锁，最终还取决于隔离级别、索引类型、查询条件和实际执行计划，不能只根据 ",[22,9343,8740],{}," 条件判断。",[18,9346,9347],{},"InnoDB 还可以锁住最后一条记录之后的间隙，从而保护开放区间查询：",[144,9349,9351],{"className":8679,"code":9350,"language":8681,"meta":150,"style":150},"SELECT * FROM orders\nWHERE amount > 200\nFOR UPDATE;\n",[22,9352,9353,9363,9374],{"__ignoreMap":150},[203,9354,9355,9357,9359,9361],{"class":205,"line":206},[203,9356,8365],{"class":209},[203,9358,8731],{"class":209},[203,9360,8734],{"class":209},[203,9362,8862],{"class":219},[203,9364,9365,9367,9369,9372],{"class":205,"line":251},[203,9366,8740],{"class":209},[203,9368,8743],{"class":219},[203,9370,9371],{"class":209},">",[203,9373,9020],{"class":718},[203,9375,9376,9378,9380],{"class":205,"line":260},[203,9377,8693],{"class":209},[203,9379,8797],{"class":209},[203,9381,1297],{"class":219},[14,9383,9385],{"id":9384},"八索引为什么很重要","八、索引为什么很重要",[18,9387,9388],{},"InnoDB 的行锁本质上是索引记录锁，Next-Key Lock 也是围绕索引扫描范围建立的。",[18,9390,9391],{},"如果查询能够使用合适索引：",[144,9393,9395],{"className":8679,"code":9394,"language":8681,"meta":150,"style":150},"SELECT * FROM orders\nWHERE amount BETWEEN 100 AND 200\nFOR UPDATE;\n",[22,9396,9397,9407,9421],{"__ignoreMap":150},[203,9398,9399,9401,9403,9405],{"class":205,"line":206},[203,9400,8365],{"class":209},[203,9402,8731],{"class":209},[203,9404,8734],{"class":209},[203,9406,8862],{"class":219},[203,9408,9409,9411,9413,9415,9417,9419],{"class":205,"line":251},[203,9410,8740],{"class":209},[203,9412,8743],{"class":219},[203,9414,8746],{"class":209},[203,9416,8749],{"class":718},[203,9418,8752],{"class":209},[203,9420,9020],{"class":718},[203,9422,9423,9425,9427],{"class":205,"line":260},[203,9424,8693],{"class":209},[203,9426,8797],{"class":209},[203,9428,1297],{"class":219},[18,9430,9431],{},"InnoDB 可以更准确地锁定相关索引范围。",[18,9433,9434],{},"如果缺少合适索引，InnoDB 可能需要扫描并锁定大量索引记录，锁的范围会变大，并发能力明显下降。",[18,9436,9437],{},"这里不要简单表述为：",[105,9439,9440],{},[18,9441,9442],{},"没有索引就一定加表锁。",[18,9444,2667],{},[105,9446,9447],{},[18,9448,9449],{},"InnoDB 仍然基于索引记录加锁；缺少可用业务索引时，可能扫描并锁住大量记录，使效果接近大范围锁定。",[18,9451,9452],{},"因此，使用锁定读避免幻读时，需要同时关注：",[159,9454,9455,9458,9461,9464,9467],{},[162,9456,9457],{},"查询条件是否命中索引；",[162,9459,9460],{},"实际执行计划选择了哪个索引；",[162,9462,9463],{},"锁定范围是否超出预期；",[162,9465,9466],{},"事务是否足够短；",[162,9468,9469],{},"是否可能形成死锁。",[14,9471,9473],{"id":9472},"九为什么不能只依赖快照读完成先查后写","九、为什么不能只依赖快照读完成“先查后写”",[18,9475,9476],{},"假设业务要求同一用户只能存在一条有效申请：",[144,9478,9480],{"className":8679,"code":9479,"language":8681,"meta":150,"style":150},"SELECT * FROM applications\nWHERE user_id = 100\n  AND status = 'ACTIVE';\n",[22,9481,9482,9493,9504],{"__ignoreMap":150},[203,9483,9484,9486,9488,9490],{"class":205,"line":206},[203,9485,8365],{"class":209},[203,9487,8731],{"class":209},[203,9489,8734],{"class":209},[203,9491,9492],{"class":219}," applications\n",[203,9494,9495,9497,9500,9502],{"class":205,"line":251},[203,9496,8740],{"class":209},[203,9498,9499],{"class":219}," user_id ",[203,9501,495],{"class":209},[203,9503,9142],{"class":718},[203,9505,9506,9509,9512,9515,9518],{"class":205,"line":260},[203,9507,9508],{"class":209},"  AND",[203,9510,9511],{"class":209}," status",[203,9513,9514],{"class":209}," =",[203,9516,9517],{"class":223}," 'ACTIVE'",[203,9519,1297],{"class":219},[18,9521,9522],{},"两个事务可能同时通过快照读发现“记录不存在”，然后各自插入一条有效申请。",[18,9524,9525],{},"REPEATABLE READ 虽然让两个事务各自的查询结果保持一致，但没有阻止另一个事务执行插入。",[18,9527,9528],{},"因此，业务不变量不能只依赖普通快照读。常见方案是：",[2695,9530,9531,9534,9537,9540],{},[162,9532,9533],{},"建立能够表达业务规则的唯一索引；",[162,9535,9536],{},"在 REPEATABLE READ 下使用合适的锁定读；",[162,9538,9539],{},"捕获重复键或死锁异常，并进行有限重试；",[162,9541,9542],{},"缩短事务时间，避免锁范围长期占用。",[18,9544,9545],{},"例如业务规则能够转换为唯一键时，优先让数据库约束兜底：",[144,9547,9549],{"className":8679,"code":9548,"language":8681,"meta":150,"style":150},"CREATE UNIQUE INDEX uk_user_active\nON applications(user_id, active_flag);\n",[22,9550,9551,9561],{"__ignoreMap":150},[203,9552,9553,9555,9558],{"class":205,"line":206},[203,9554,9045],{"class":209},[203,9556,9557],{"class":209}," UNIQUE INDEX",[203,9559,9560],{"class":215}," uk_user_active\n",[203,9562,9563,9566],{"class":205,"line":251},[203,9564,9565],{"class":209},"ON",[203,9567,9568],{"class":219}," applications(user_id, active_flag);\n",[18,9570,9571],{},"具体索引设计仍要根据数据模型确定，不能为了唯一约束直接照搬示例。",[14,9573,9575],{"id":9574},"十混用快照读和当前读为什么容易困惑","十、混用快照读和当前读为什么容易困惑",[18,9577,9578,9579,9581],{},"在 REPEATABLE READ 事务中，普通 ",[22,9580,8365],{}," 使用旧快照，而锁定读读取最新可用的数据。",[18,9583,9584],{},"可能出现：",[144,9586,9589],{"className":9587,"code":9588,"language":149,"meta":150},[147],"1. 普通 SELECT 没看到某条新记录\n2. 另一个事务插入该记录并提交\n3. SELECT ... FOR UPDATE 却看到了这条记录\n",[22,9590,9588],{"__ignoreMap":150},[18,9592,9593],{},"原因不是隔离级别突然失效，而是两条语句使用了不同的读取机制：",[159,9595,9596,9601],{},[162,9597,8362,9598,9600],{},[22,9599,8365],{},"：读取 Read View 中的一致性快照；",[162,9602,9603,9605],{},[22,9604,8338],{},"：读取最新可用数据并加锁。",[18,9607,9608],{},"MySQL 官方文档也不建议在同一个 REPEATABLE READ 事务中随意混用锁定语句和非锁定查询，因为它们可能呈现两个不同时间状态的数据。",[18,9610,9611],{},"如果业务必须基于最新结果做写入判断，应统一使用锁定读；如果必须获得更严格、容易理解的串行语义，可以评估 SERIALIZABLE，但要接受更高的锁竞争。",[14,9613,9615],{"id":9614},"十一read-committed-下加-for-update-能否避免幻读","十一、READ COMMITTED 下加 FOR UPDATE 能否避免幻读",[18,9617,9618],{},"不能笼统地说可以。",[18,9620,9621,9622,40,9624,9626],{},"在 READ COMMITTED 下，InnoDB 对锁定读、",[22,9623,8344],{},[22,9625,8347],{}," 通常只锁索引记录，不锁前面的间隙。其他事务仍可以向记录间隙插入新行，因此范围查询仍可能出现幻读。",[18,9628,9629],{},"Gap Lock 在该级别主要保留给：",[159,9631,9632,9635],{},[162,9633,9634],{},"外键约束检查；",[162,9636,9637],{},"重复键检查。",[18,9639,9640],{},"如果业务需要锁住一个尚不存在的范围，应评估：",[159,9642,9643,9646,9649,9652],{},[162,9644,9645],{},"使用 REPEATABLE READ，并通过合适索引和 Next-Key Lock 锁定范围；",[162,9647,9648],{},"使用 SERIALIZABLE；",[162,9650,9651],{},"把业务不变量转换为唯一约束；",[162,9653,9654],{},"调整数据模型，避免依赖无法锁定的“空结果”。",[14,9656,9658],{"id":9657},"十二如何选择隔离级别","十二、如何选择隔离级别",[473,9660,9662],{"id":9661},"选择-read-committed","选择 READ COMMITTED",[18,9664,4332],{},[159,9666,9667,9670,9673,9676],{},[162,9668,9669],{},"希望每次查询都看到最新已提交数据；",[162,9671,9672],{},"可以接受同一事务内两次读取结果不同；",[162,9674,9675],{},"希望减少 Gap Lock 和锁冲突；",[162,9677,9678],{},"业务通过版本号、唯一约束或其他机制保证并发正确性。",[473,9680,9682],{"id":9681},"选择-repeatable-read","选择 REPEATABLE READ",[18,9684,4332],{},[159,9686,9687,9690,9693,9696],{},[162,9688,9689],{},"希望事务中的普通查询保持一致视图；",[162,9691,9692],{},"需要使用 Next-Key Lock 保护范围；",[162,9694,9695],{},"能够控制索引、锁顺序和事务时长；",[162,9697,9698],{},"接受 InnoDB 默认隔离级别。",[473,9700,9702],{"id":9701},"选择-serializable","选择 SERIALIZABLE",[18,9704,4332],{},[159,9706,9707,9710,9713,9716],{},[162,9708,9709],{},"并发量较低；",[162,9711,9712],{},"一致性要求极高；",[162,9714,9715],{},"业务逻辑难以通过更细粒度约束实现；",[162,9717,9718],{},"能够接受更多阻塞、超时和死锁重试。",[18,9720,9721],{},"隔离级别不是越高越好。选择时需要综合评估：",[159,9723,9724,9727,9730,9733,9736,9739],{},[162,9725,9726],{},"一致性要求；",[162,9728,9729],{},"读写比例；",[162,9731,9732],{},"热点数据；",[162,9734,9735],{},"事务持续时间；",[162,9737,9738],{},"可接受的锁等待；",[162,9740,9741],{},"数据库吞吐量。",[14,9743,9745],{"id":9744},"十三查看和设置隔离级别","十三、查看和设置隔离级别",[18,9747,9748],{},"查看当前会话隔离级别：",[144,9750,9752],{"className":8679,"code":9751,"language":8681,"meta":150,"style":150},"SELECT @@SESSION.transaction_isolation;\n",[22,9753,9754],{"__ignoreMap":150},[203,9755,9756,9758,9761,9764,9767,9770],{"class":205,"line":206},[203,9757,8365],{"class":209},[203,9759,9760],{"class":219}," @@",[203,9762,9763],{"class":718},"SESSION",[203,9765,9766],{"class":219},".",[203,9768,9769],{"class":718},"transaction_isolation",[203,9771,1297],{"class":219},[18,9773,9774],{},"查看全局默认隔离级别：",[144,9776,9778],{"className":8679,"code":9777,"language":8681,"meta":150,"style":150},"SELECT @@GLOBAL.transaction_isolation;\n",[22,9779,9780],{"__ignoreMap":150},[203,9781,9782,9784,9786,9789,9791,9793],{"class":205,"line":206},[203,9783,8365],{"class":209},[203,9785,9760],{"class":219},[203,9787,9788],{"class":718},"GLOBAL",[203,9790,9766],{"class":219},[203,9792,9769],{"class":718},[203,9794,1297],{"class":219},[18,9796,9797],{},"修改当前会话后续事务的隔离级别：",[144,9799,9801],{"className":8679,"code":9800,"language":8681,"meta":150,"style":150},"SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;\n",[22,9802,9803],{"__ignoreMap":150},[203,9804,9805,9808,9811,9814,9817,9820,9823,9826],{"class":205,"line":206},[203,9806,9807],{"class":209},"SET",[203,9809,9810],{"class":209}," SESSION",[203,9812,9813],{"class":209}," TRANSACTION",[203,9815,9816],{"class":209}," ISOLATION",[203,9818,9819],{"class":209}," LEVEL",[203,9821,9822],{"class":209}," READ",[203,9824,9825],{"class":209}," COMMITTED",[203,9827,1297],{"class":219},[18,9829,9830],{},"设置下一个事务的隔离级别：",[144,9832,9834],{"className":8679,"code":9833,"language":8681,"meta":150,"style":150},"SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;\n",[22,9835,9836],{"__ignoreMap":150},[203,9837,9838,9840,9842,9844,9846,9849,9851],{"class":205,"line":206},[203,9839,9807],{"class":209},[203,9841,9813],{"class":209},[203,9843,9816],{"class":209},[203,9845,9819],{"class":209},[203,9847,9848],{"class":209}," REPEATABLE",[203,9850,9822],{"class":209},[203,9852,1297],{"class":219},[18,9854,9855],{},"开始事务：",[144,9857,9859],{"className":8679,"code":9858,"language":8681,"meta":150,"style":150},"START TRANSACTION;\n",[22,9860,9861],{"__ignoreMap":150},[203,9862,9863,9865],{"class":205,"line":206},[203,9864,8845],{"class":209},[203,9866,1297],{"class":219},[18,9868,9869],{},"线上修改隔离级别前，需要：",[159,9871,9872,9875,9882],{},[162,9873,9874],{},"验证依赖一致性快照、锁定范围和“先查后写”的业务逻辑；",[162,9876,9877,9878,9881],{},"确认 ",[22,9879,9880],{},"binlog_format"," 与目标隔离级别兼容；",[162,9883,9884],{},"观察切换前后的锁等待、死锁率、事务时延和数据库吞吐量。",[18,9886,9887,9888,9891,9892,9895],{},"这里需要确认的不是笼统的“复制方式”，而是二进制日志格式的兼容性。例如 MySQL 的 READ COMMITTED 只支持 Row-Based Logging；如果配置为 ",[22,9889,9890],{},"MIXED","，MySQL 会自动使用行格式记录。系统原本已经使用 ",[22,9893,9894],{},"ROW"," 时，通常不需要为此额外调整。",[18,9897,9898],{},"监控指标也不是选择隔离级别的前置条件，而是用于验证切换产生的实际影响：",[159,9900,9901,9904],{},[162,9902,9903],{},"从 REPEATABLE READ 切换到 READ COMMITTED，Gap Lock 通常减少，锁等待和死锁概率可能下降，但业务会失去事务级固定快照；",[162,9905,9906],{},"切换到更高隔离级别时，锁范围和等待时间可能增加，事务耗时及吞吐量可能受到影响。",[18,9908,9909],{},"因此，不能只根据理论吞吐量直接切换隔离级别。",[14,9911,4495],{"id":4494},[473,9913,9915],{"id":9914},"误区-1mvcc-会阻止其他事务插入","误区 1：MVCC 会阻止其他事务插入",[18,9917,9918],{},"不会。MVCC 主要决定一条记录对当前 Read View 是否可见，不会因为快照读而阻塞其他事务写入。",[473,9920,9922],{"id":9921},"误区-2repeatable-read-只靠-mvcc-避免所有幻读","误区 2：REPEATABLE READ 只靠 MVCC 避免所有幻读",[18,9924,9925],{},"不完整。",[159,9927,9928,9931],{},[162,9929,9930],{},"快照读靠 MVCC 和固定 Read View；",[162,9932,9933],{},"当前读靠 Gap Lock 和 Next-Key Lock。",[473,9935,9937],{"id":9936},"误区-3只要加-for-update-就一定不会幻读","误区 3：只要加 FOR UPDATE 就一定不会幻读",[18,9939,9940],{},"不一定。是否锁住间隙与隔离级别、查询条件、索引类型和实际扫描范围有关。READ COMMITTED 下通常不会为普通范围查询使用 Gap Lock。",[473,9942,9944],{"id":9943},"误区-4next-key-lock-锁的是数据页","误区 4：Next-Key Lock 锁的是数据页",[18,9946,9947],{},"不是。它围绕索引记录和索引记录之间的间隙建立。",[473,9949,9951],{"id":9950},"误区-5没有索引就一定升级为表锁","误区 5：没有索引就一定升级为表锁",[18,9953,9954],{},"这个表述不准确。InnoDB 仍然基于索引进行记录锁定，但缺少合适索引可能导致扫描和锁定大量记录，使并发效果接近大范围锁定。",[473,9956,9958],{"id":9957},"误区-6serializable-一定是最好的选择","误区 6：SERIALIZABLE 一定是最好的选择",[18,9960,9961],{},"它的隔离性最强，但会显著增加锁等待，降低并发能力。多数业务更适合通过合理隔离级别、索引、锁定读和数据库约束共同保证正确性。",[14,9963,5803],{"id":5803},[159,9965,9966,9973,9980,9987,9994],{},[162,9967,9968],{},[119,9969,9972],{"href":9970,"rel":9971},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-transaction-isolation-levels.html",[2080],"MySQL 8.4 Reference Manual：Transaction Isolation Levels",[162,9974,9975],{},[119,9976,9979],{"href":9977,"rel":9978},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-consistent-read.html",[2080],"MySQL 8.4 Reference Manual：Consistent Nonlocking Reads",[162,9981,9982],{},[119,9983,9986],{"href":9984,"rel":9985},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-next-key-locking.html",[2080],"MySQL 8.4 Reference Manual：Phantom Rows",[162,9988,9989],{},[119,9990,9993],{"href":9991,"rel":9992},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-locking-reads.html",[2080],"MySQL 8.4 Reference Manual：Locking Reads",[162,9995,9996],{},[119,9997,10000],{"href":9998,"rel":9999},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Fset-transaction.html",[2080],"MySQL 8.4 Reference Manual：SET TRANSACTION Statement",[2111,10002,10003],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":150,"searchDepth":251,"depth":251,"links":10005},[10006,10009,10010,10011,10012,10017,10023,10027,10028,10029,10034,10035,10036,10037,10038,10043,10044,10052],{"id":16,"depth":251,"text":16,"children":10007},[10008],{"id":8377,"depth":260,"text":8377},{"id":115,"depth":251,"text":115},{"id":132,"depth":251,"text":132},{"id":8398,"depth":251,"text":8399},{"id":8504,"depth":251,"text":8505,"children":10013},[10014,10015,10016],{"id":8508,"depth":260,"text":8509},{"id":8527,"depth":260,"text":8528},{"id":8548,"depth":260,"text":8549},{"id":8583,"depth":251,"text":8584,"children":10018},[10019,10020,10021,10022],{"id":8587,"depth":260,"text":8588},{"id":8608,"depth":260,"text":8609},{"id":8641,"depth":260,"text":8642},{"id":8666,"depth":260,"text":8667},{"id":8705,"depth":251,"text":8706,"children":10024},[10025,10026],{"id":8712,"depth":260,"text":8713},{"id":8777,"depth":260,"text":8778},{"id":8831,"depth":251,"text":8832},{"id":8976,"depth":251,"text":8977},{"id":9105,"depth":251,"text":9106,"children":10030},[10031,10032,10033],{"id":9109,"depth":260,"text":9110},{"id":9159,"depth":260,"text":9160},{"id":9187,"depth":260,"text":9188},{"id":9384,"depth":251,"text":9385},{"id":9472,"depth":251,"text":9473},{"id":9574,"depth":251,"text":9575},{"id":9614,"depth":251,"text":9615},{"id":9657,"depth":251,"text":9658,"children":10039},[10040,10041,10042],{"id":9661,"depth":260,"text":9662},{"id":9681,"depth":260,"text":9682},{"id":9701,"depth":260,"text":9702},{"id":9744,"depth":251,"text":9745},{"id":4494,"depth":251,"text":4495,"children":10045},[10046,10047,10048,10049,10050,10051],{"id":9914,"depth":260,"text":9915},{"id":9921,"depth":260,"text":9922},{"id":9936,"depth":260,"text":9937},{"id":9943,"depth":260,"text":9944},{"id":9950,"depth":260,"text":9951},{"id":9957,"depth":260,"text":9958},{"id":5803,"depth":251,"text":5803},"wiki:java:mysql-transaction-isolation-and-phantom-read","梳理 MySQL InnoDB 的四种事务隔离级别，解释脏读、不可重复读和幻读，并说明 MVCC、Read View、Gap Lock 与 Next-Key Lock 如何共同避免幻读。",{},75,"\u002Fjava\u002Fmysql-transaction-isolation-and-phantom-read",{"title":8308,"description":10054},"java\u002Fmysql-transaction-isolation-and-phantom-read","fQEdrnJPxztc16zbaEZBXfxB2OYyLN5Gdv5shRJ6FbQ",{"id":10062,"title":10063,"body":10064,"commentId":11200,"description":11201,"difficulty":3667,"draft":2148,"extension":2149,"meta":11202,"navigation":756,"order":11203,"path":11204,"section":2153,"seo":11205,"stem":11206,"updated":11207,"__hash__":11208},"java\u002Fjava\u002Fmysql-bplus-tree-vs-btree.md","MySQL 为何采用 B+ 树？B+ 树与 B 树的区别是什么？",{"type":7,"value":10065,"toc":11169},[10066,10068,10071,10074,10088,10091,10094,10096,10101,10103,10112,10114,10118,10125,10145,10148,10150,10164,10168,10171,10174,10177,10183,10186,10203,10210,10214,10217,10220,10226,10229,10245,10248,10262,10266,10370,10373,10376,10380,10383,10386,10389,10395,10398,10405,10410,10414,10419,10422,10427,10430,10436,10439,10445,10448,10454,10457,10461,10464,10467,10473,10476,10479,10482,10488,10491,10497,10500,10505,10514,10518,10521,10558,10565,10571,10574,10580,10583,10586,10628,10631,10635,10638,10641,10647,10650,10657,10661,10664,10679,10682,10685,10735,10738,10741,10758,10761,10764,10768,10771,10774,10789,10792,10798,10801,10827,10830,10833,10837,10840,10846,10849,10873,10876,10909,10914,10920,10923,10926,10956,10959,10968,10972,10975,10978,10984,10987,10990,10994,10997,11014,11017,11021,11024,11027,11033,11036,11039,11053,11056,11060,11064,11067,11071,11074,11078,11081,11085,11088,11092,11103,11107,11110,11116,11122,11127,11129,11166],[14,10067,16],{"id":16},[18,10069,10070],{},"MySQL InnoDB 的普通索引采用通常所说的 B+ 树结构，核心原因是数据库索引需要同时兼顾磁盘或页访问次数、等值查询、范围查询和顺序遍历。",[18,10072,10073],{},"B+ 树与 B 树的主要区别是：",[159,10075,10076,10079,10082,10085],{},[162,10077,10078],{},"B 树的非叶子节点和叶子节点都可以保存数据记录；",[162,10080,10081],{},"B+ 树的非叶子节点主要保存索引键和子页面指针，完整索引记录集中在叶子节点；",[162,10083,10084],{},"B+ 树的叶子节点按照键值顺序连接，适合范围扫描；",[162,10086,10087],{},"由于非叶子节点不保存完整数据，同样大小的页面能够容纳更多目录项，树的扇出更大、高度更低。",[18,10089,10090],{},"InnoDB 默认索引页大小为 16KB。一次读取通常以页为单位，而不是只读取一个键。B+ 树通过较大的扇出，把大量数据组织在较少的层级中，查询通常只需要访问少量页面；定位到范围起点后，还可以沿叶子页顺序扫描，不必反复从根节点查找。",[18,10092,10093],{},"InnoDB 的聚簇索引叶子节点保存完整行数据，二级索引叶子节点保存二级索引键和主键值。通过二级索引查询非覆盖列时，需要先取得主键，再回到聚簇索引查找完整行，这就是常说的回表。",[473,10095,8377],{"id":8377},[105,10097,10098],{},[18,10099,10100],{},"B+ 树让非叶子节点专注于导航，以更大的扇出降低树高，并把有序数据集中在相连的叶子节点中，因此既能减少页面访问，又适合范围查询和顺序扫描。",[14,10102,115],{"id":115},[18,10104,10105],{},[119,10106,10109],{"href":10107,"target":122,"rel":10108},"\u002Fimages\u002Fwiki\u002Fjava\u002Fmysql-btree-vs-bplus-tree.svg",[124,125],[127,10110],{"src":10107,"alt":10111},"B 树与 B+ 树结构对比",[14,10113,132],{"id":132},[14,10115,10117],{"id":10116},"一先明确mysql-文档中的-b-tree-与-b-树","一、先明确：MySQL 文档中的 B-tree 与 B+ 树",[18,10119,10120,10121,10124],{},"MySQL 官方文档通常把 InnoDB 的普通索引统称为 ",[22,10122,10123],{},"B-tree"," 索引。在数据库实现和面试语境中，InnoDB 的这种索引组织通常被称为 B+ 树：",[159,10126,10127,10130,10133,10136,10139,10142],{},[162,10128,10129],{},"索引记录存放在叶子页；",[162,10131,10132],{},"非叶子页负责索引导航；",[162,10134,10135],{},"聚簇索引的叶子页保存行数据；",[162,10137,10138],{},"二级索引的叶子记录保存索引列和主键；",[162,10140,10141],{},"InnoDB 同一层的索引页通过前后页指针双向连接；",[162,10143,10144],{},"叶子页中的索引记录有序，能够进行范围扫描。",[18,10146,10147],{},"因此，“MySQL 使用 B+ 树”通常是在描述 InnoDB 普通索引的具体组织特征；它不代表 MySQL 中所有索引都使用 B+ 树。",[18,10149,686],{},[159,10151,10152,10155,10158,10161],{},[162,10153,10154],{},"InnoDB 普通索引使用 B-tree 类结构；",[162,10156,10157],{},"空间索引使用 R-tree；",[162,10159,10160],{},"全文索引有独立的倒排索引设计；",[162,10162,10163],{},"InnoDB 还可能使用自适应哈希索引加速部分热点等值访问。",[14,10165,10167],{"id":10166},"二b-树是什么","二、B 树是什么",[18,10169,10170],{},"B 树是一种多路平衡查找树。",[18,10172,10173],{},"“多路”表示一个节点可以拥有多个子节点，而不是像二叉查找树那样最多只有两个子节点。",[18,10175,10176],{},"一个简化的 B 树可以表示为：",[144,10178,10181],{"className":10179,"code":10180,"language":149,"meta":150},[147],"                 [20 | 50]\n                \u002F    |    \\\n       [5 | 10]   [30 | 40]   [60 | 80]\n",[22,10182,10180],{"__ignoreMap":150},[18,10184,10185],{},"B 树具有以下特点：",[159,10187,10188,10191,10194,10197,10200],{},[162,10189,10190],{},"节点中的键有序；",[162,10192,10193],{},"一个节点可以保存多个键和数据记录；",[162,10195,10196],{},"非叶子节点也可以直接保存数据；",[162,10198,10199],{},"所有叶子节点通常处于同一层；",[162,10201,10202],{},"插入和删除后通过分裂、合并或旋转维持平衡。",[18,10204,10205,10206,10209],{},"查询 ",[22,10207,10208],{},"20"," 时，可能在根节点直接找到数据，不一定需要访问叶子节点。",[14,10211,10213],{"id":10212},"三b-树是什么","三、B+ 树是什么",[18,10215,10216],{},"B+ 树也是多路平衡查找树，但它把导航结构和数据记录进一步分离。",[18,10218,10219],{},"一个简化的 B+ 树可以表示为：",[144,10221,10224],{"className":10222,"code":10223,"language":149,"meta":150},[147],"                         [20 | 50]\n                        \u002F    |    \\\n                       \u002F     |     \\\n        [5 | 10 | 20] [30 | 40 | 50] [60 | 80]\n                ↔              ↔\n          叶子页通过前后页指针双向连接\n",[22,10225,10223],{"__ignoreMap":150},[18,10227,10228],{},"B+ 树具有以下典型特点：",[159,10230,10231,10234,10237,10240,10242],{},[162,10232,10233],{},"非叶子节点主要保存键和子节点指针；",[162,10235,10236],{},"完整的数据记录集中在叶子节点；",[162,10238,10239],{},"所有查询最终都会到达叶子节点；",[162,10241,10141],{},[162,10243,10244],{},"适合从某个键开始连续读取一段数据。",[18,10246,10247],{},"需要注意，示意图中的分隔键可能同时出现在非叶子节点和叶子节点中。非叶子节点中的键用于导航，叶子节点中的索引记录才承载最终数据定位信息。",[18,10249,10250,10251,10254,10255,53,10258,10261],{},"这里的双向链表连接的是",[110,10252,10253],{},"索引页","，并不是说整棵 B+ 树本身就是双向链表。整体结构仍然是一棵多路平衡树：树结构负责快速定位目标页；页面头中的 ",[22,10256,10257],{},"FIL_PAGE_PREV",[22,10259,10260],{},"FIL_PAGE_NEXT"," 字段保存前后页的页号，这些页号相当于逻辑指针，用于前后遍历。单个索引页内部的记录则主要按键值顺序组成单向链表，并由页目录辅助查找。",[14,10263,10265],{"id":10264},"四b-树与-b-树的核心区别","四、B 树与 B+ 树的核心区别",[299,10267,10268,10280],{},[302,10269,10270],{},[305,10271,10272,10274,10277],{},[308,10273,950],{},[308,10275,10276],{},"B 树",[308,10278,10279],{},"B+ 树",[315,10281,10282,10293,10304,10315,10326,10337,10348,10359],{},[305,10283,10284,10287,10290],{},[320,10285,10286],{},"数据记录位置",[320,10288,10289],{},"非叶子节点和叶子节点都可以保存",[320,10291,10292],{},"完整索引记录集中在叶子节点",[305,10294,10295,10298,10301],{},[320,10296,10297],{},"非叶子节点职责",[320,10299,10300],{},"导航，同时可能保存数据",[320,10302,10303],{},"主要负责导航",[305,10305,10306,10309,10312],{},[320,10307,10308],{},"单个节点可容纳的键",[320,10310,10311],{},"相对较少",[320,10313,10314],{},"通常更多",[305,10316,10317,10320,10323],{},[320,10318,10319],{},"树的扇出",[320,10321,10322],{},"相对较小",[320,10324,10325],{},"通常更大",[305,10327,10328,10331,10334],{},[320,10329,10330],{},"树高",[320,10332,10333],{},"相同数据量下可能更高",[320,10335,10336],{},"相同数据量下通常更低",[305,10338,10339,10342,10345],{},[320,10340,10341],{},"等值查询",[320,10343,10344],{},"可能在非叶子节点提前结束",[320,10346,10347],{},"通常需要走到叶子节点",[305,10349,10350,10353,10356],{},[320,10351,10352],{},"范围查询",[320,10354,10355],{},"可能需要在树中多次定位",[320,10357,10358],{},"定位起点后可顺序扫描叶子节点",[305,10360,10361,10364,10367],{},[320,10362,10363],{},"查询路径",[320,10365,10366],{},"不同记录的结束层级可能不同",[320,10368,10369],{},"通常都到叶子层，路径更稳定",[18,10371,10372],{},"不能简单得出“B+ 树在所有场景都比 B 树快”的结论。",[18,10374,10375],{},"如果整个数据结构都在内存中，而且只做等值查询，B 树可能在非叶子节点提前命中。但数据库索引更关心页面访问、范围扫描和批量读取，B+ 树的整体特性更适合这类负载。",[14,10377,10379],{"id":10378},"五为什么数据库关心页面访问次数","五、为什么数据库关心页面访问次数",[18,10381,10382],{},"InnoDB 管理数据的基本单位是页，默认页面大小为 16KB。",[18,10384,10385],{},"查询一个索引键时，底层不是只从磁盘读取这个键对应的几个字节，而是把相关页面读入 Buffer Pool，再在页内查找。",[18,10387,10388],{},"因此，索引设计要尽量做到：",[144,10390,10393],{"className":10391,"code":10392,"language":149,"meta":150},[147],"更少的树层级\n        ↓\n更少的页面访问\n        ↓\n更低的 I\u002FO 和缓存查找成本\n",[22,10394,10392],{"__ignoreMap":150},[18,10396,10397],{},"即使相关页面已经在 Buffer Pool 中，查询仍然需要逐层定位索引页、在页内比较目录项，并在必要时获取页面 Latch。树高较低通常意味着遍历的页面更少，但此时节省的主要是内存中的索引导航成本，而不是磁盘 I\u002FO。",[18,10399,10400,10401,10404],{},"所以分析 B+ 树时，不应只背诵时间复杂度 ",[22,10402,10403],{},"O(log N)","，还要关注：",[105,10406,10407],{},[18,10408,10409],{},"这个对数以多大的扇出为底，以及一次查询需要访问多少个页面。",[14,10411,10413],{"id":10412},"六扇出是什么","六、扇出是什么",[18,10415,10416],{},[110,10417,10418],{},"扇出（Fan-out）是一个非叶子节点能够直接指向的子节点数量。",[18,10420,10421],{},"在 InnoDB 中，可以把它理解为：",[105,10423,10424],{},[18,10425,10426],{},"一个非叶子索引页能够直接导航到多少个下层索引页。",[18,10428,10429],{},"例如，某个非叶子页保存了大量“索引值 + 子页面指针”目录项，并能够据此导航到约 1,000 个子页面，那么可以近似地说这个节点的扇出约为 1,000。",[18,10431,10432,10433,33],{},"扇出不是整棵树的记录总数，也不是一个叶子页能够保存的行数。它描述的是",[110,10434,10435],{},"单个非叶子节点向下一层分出的分支数量",[18,10437,10438],{},"对于相同数量的数据，树高可以粗略理解为：",[144,10440,10443],{"className":10441,"code":10442,"language":149,"meta":150},[147],"树高 ≈ log(扇出)(需要管理的页面数)\n",[22,10444,10442],{"__ignoreMap":150},[18,10446,10447],{},"扇出越大，每增加一层能够覆盖的页面数增长得越快，因此管理相同数据量所需的层级通常越少：",[144,10449,10452],{"className":10450,"code":10451,"language":149,"meta":150},[147],"扇出为 2：       2 → 4 → 8 → 16\n扇出为 1,000：   1,000 → 1,000,000 → 1,000,000,000\n",[22,10453,10451],{"__ignoreMap":150},[18,10455,10456],{},"这里的数字只用于说明增长速度。真实扇出会受到索引键长度、页面格式、记录头、页面填充率等因素影响。",[14,10458,10460],{"id":10459},"七b-树如何通过更大的扇出降低树高","七、B+ 树如何通过更大的扇出降低树高",[18,10462,10463],{},"假设一个非叶子页既保存键，又保存完整行数据，那么每个目录项会比较大，一个页面能够容纳的目录项就比较少。",[18,10465,10466],{},"如果非叶子页只保存：",[144,10468,10471],{"className":10469,"code":10470,"language":149,"meta":150},[147],"索引键 + 子页面指针\n",[22,10472,10470],{"__ignoreMap":150},[18,10474,10475],{},"每个目录项更小，同一个 16KB 页面就能容纳更多目录项，一个节点可以指向更多子页面。",[18,10477,10478],{},"由于单个目录项更紧凑，同一个页面可以保存更多目录项、指向更多子页面，这就是 B+ 树扇出更大的原因。",[18,10480,10481],{},"例如，仅用于理解数量级，假设一个非叶子目录项平均占用 16 字节，忽略页头、槽目录、填充率和其他开销：",[144,10483,10486],{"className":10484,"code":10485,"language":149,"meta":150},[147],"16KB ÷ 16B ≈ 1024\n",[22,10487,10485],{"__ignoreMap":150},[18,10489,10490],{},"一个非叶子页理论上可以导航约一千个子页面：",[144,10492,10495],{"className":10493,"code":10494,"language":149,"meta":150},[147],"根节点：约 1,000 个分支\n第二层：约 1,000 × 1,000 个分支\n第三层：继续扩大\n",[22,10496,10494],{"__ignoreMap":150},[18,10498,10499],{},"真实容量会受到键长度、页格式、记录头、填充率等因素影响，不能直接按这个示例计算生产容量。但它能说明：",[105,10501,10502],{},[18,10503,10504],{},"多路树的扇出很大，少量层级就可以管理大量记录。",[18,10506,10507],{},[119,10508,10511],{"href":10509,"target":122,"rel":10510},"\u002Fimages\u002Fwiki\u002Fjava\u002Fmysql-bplus-tree-fanout.svg",[124,125],[127,10512],{"src":10509,"alt":10513},"B+ 树通过大扇出降低树高",[14,10515,10517],{"id":10516},"八为什么-b-树适合范围查询","八、为什么 B+ 树适合范围查询",[18,10519,10520],{},"假设执行：",[144,10522,10524],{"className":8679,"code":10523,"language":8681,"meta":150,"style":150},"SELECT *\nFROM orders\nWHERE id BETWEEN 1000 AND 2000;\n",[22,10525,10526,10533,10540],{"__ignoreMap":150},[203,10527,10528,10530],{"class":205,"line":206},[203,10529,8365],{"class":209},[203,10531,10532],{"class":209}," *\n",[203,10534,10535,10538],{"class":205,"line":251},[203,10536,10537],{"class":209},"FROM",[203,10539,8862],{"class":219},[203,10541,10542,10544,10546,10548,10551,10553,10556],{"class":205,"line":260},[203,10543,8740],{"class":209},[203,10545,9137],{"class":219},[203,10547,8746],{"class":209},[203,10549,10550],{"class":718}," 1000",[203,10552,8752],{"class":209},[203,10554,10555],{"class":718}," 2000",[203,10557,1297],{"class":219},[18,10559,10560,10561,10564],{},"B+ 树可以先从根节点定位到 ",[22,10562,10563],{},"id=1000"," 附近的叶子页：",[144,10566,10569],{"className":10567,"code":10568,"language":149,"meta":150},[147],"根页\n  ↓\n非叶子页\n  ↓\n包含 1000 的叶子页\n",[22,10570,10568],{"__ignoreMap":150},[18,10572,10573],{},"定位范围起点以后，可以沿叶子页顺序扫描：",[144,10575,10578],{"className":10576,"code":10577,"language":149,"meta":150},[147],"[1000 ... 1200] ↔ [1201 ... 1500] ↔ [1501 ... 1800] ↔ [1801 ... 2000]\n",[22,10579,10577],{"__ignoreMap":150},[18,10581,10582],{},"不需要为范围中的每一条记录重新从根节点查找。",[18,10584,10585],{},"因此，B+ 树天然适合：",[159,10587,10588,10600,10604,10610,10616,10625],{},[162,10589,10590,53,10592,53,10595,53,10597,2365],{},[22,10591,9371],{},[22,10593,10594],{},">=",[22,10596,3306],{},[22,10598,10599],{},"\u003C=",[162,10601,10602,2365],{},[22,10603,8746],{},[162,10605,10606,10607,2365],{},"有效前缀的 ",[22,10608,10609],{},"LIKE 'abc%'",[162,10611,10612,10613,2365],{},"按索引顺序执行的 ",[22,10614,10615],{},"ORDER BY",[162,10617,10618,10619,53,10622,2365],{},"部分 ",[22,10620,10621],{},"MIN",[22,10623,10624],{},"MAX",[162,10626,10627],{},"范围扫描和顺序分页。",[18,10629,10630],{},"能否真正使用索引顺序，还取决于联合索引结构、最左前缀、排序方向和执行计划，不能因为底层是 B+ 树就认为所有范围条件都会高效。",[14,10632,10634],{"id":10633},"九为什么不用二叉查找树或红黑树","九、为什么不用二叉查找树或红黑树",[18,10636,10637],{},"二叉查找树和红黑树的扇出通常是 2。",[18,10639,10640],{},"即使红黑树能够保持近似平衡，管理大量数据时树高仍然明显高于多路树：",[144,10642,10645],{"className":10643,"code":10644,"language":149,"meta":150},[147],"二叉树：\n每个节点最多 2 个分支\n\nB+ 树：\n每个页面可以拥有数百甚至更多分支\n",[22,10646,10644],{"__ignoreMap":150},[18,10648,10649],{},"如果每层节点可能位于不同页面，树越高，查询可能需要访问的页面越多。",[18,10651,10652,10653,10656],{},"红黑树很适合内存中的集合和映射，例如 Java 的 ",[22,10654,10655],{},"TreeMap","；数据库索引需要尽量减少页面 I\u002FO，并支持大范围顺序扫描，因此多路 B+ 树更加合适。",[14,10658,10660],{"id":10659},"十为什么不直接使用哈希索引","十、为什么不直接使用哈希索引",[18,10662,10663],{},"哈希结构擅长等值查询：",[144,10665,10667],{"className":8679,"code":10666,"language":8681,"meta":150,"style":150},"WHERE id = 100\n",[22,10668,10669],{"__ignoreMap":150},[203,10670,10671,10673,10675,10677],{"class":205,"line":206},[203,10672,8740],{"class":209},[203,10674,9137],{"class":219},[203,10676,495],{"class":209},[203,10678,9142],{"class":718},[18,10680,10681],{},"理想情况下可以根据哈希值直接定位桶。",[18,10683,10684],{},"但哈希值不保留原始键的顺序，因此不适合：",[144,10686,10688],{"className":8679,"code":10687,"language":8681,"meta":150,"style":150},"WHERE id > 100;\nWHERE id BETWEEN 100 AND 200;\nORDER BY id;\nLIKE 'abc%';\n",[22,10689,10690,10702,10718,10725],{"__ignoreMap":150},[203,10691,10692,10694,10696,10698,10700],{"class":205,"line":206},[203,10693,8740],{"class":209},[203,10695,9137],{"class":219},[203,10697,9371],{"class":209},[203,10699,8749],{"class":718},[203,10701,1297],{"class":219},[203,10703,10704,10706,10708,10710,10712,10714,10716],{"class":205,"line":251},[203,10705,8740],{"class":209},[203,10707,9137],{"class":219},[203,10709,8746],{"class":209},[203,10711,8749],{"class":718},[203,10713,8752],{"class":209},[203,10715,8755],{"class":718},[203,10717,1297],{"class":219},[203,10719,10720,10722],{"class":205,"line":260},[203,10721,10615],{"class":209},[203,10723,10724],{"class":219}," id;\n",[203,10726,10727,10730,10733],{"class":205,"line":278},[203,10728,10729],{"class":209},"LIKE",[203,10731,10732],{"class":223}," 'abc%'",[203,10734,1297],{"class":219},[18,10736,10737],{},"哈希索引还需要处理哈希冲突，也无法自然支持联合索引的有序最左前缀扫描。",[18,10739,10740],{},"B+ 树虽然等值查找通常需要沿树下降，但同时支持：",[159,10742,10743,10746,10749,10752,10755],{},[162,10744,10745],{},"等值查询；",[162,10747,10748],{},"范围查询；",[162,10750,10751],{},"顺序扫描；",[162,10753,10754],{},"排序；",[162,10756,10757],{},"联合索引前缀匹配。",[18,10759,10760],{},"因此它的通用性更强。",[18,10762,10763],{},"InnoDB 的自适应哈希索引可以为部分热点 B-tree 页面建立哈希访问路径，但它属于内部优化，不会替代原有 B+ 树索引。",[14,10765,10767],{"id":10766},"十一innodb-聚簇索引如何组织数据","十一、InnoDB 聚簇索引如何组织数据",[18,10769,10770],{},"每张 InnoDB 表都有一个聚簇索引。",[18,10772,10773],{},"聚簇索引的选择顺序通常是：",[2695,10775,10776,10779,10786],{},[162,10777,10778],{},"显式定义的主键；",[162,10780,10781,10782,10785],{},"第一个所有列均为 ",[22,10783,10784],{},"NOT NULL"," 的唯一索引；",[162,10787,10788],{},"InnoDB 自动生成的隐藏行 ID。",[18,10790,10791],{},"聚簇索引可以简化为：",[144,10793,10796],{"className":10794,"code":10795,"language":149,"meta":150},[147],"非叶子节点：\n主键 + 子页面指针\n\n叶子节点：\n主键 + 完整行数据\n",[22,10797,10795],{"__ignoreMap":150},[18,10799,10800],{},"因此，通过主键查询：",[144,10802,10804],{"className":8679,"code":10803,"language":8681,"meta":150,"style":150},"SELECT * FROM users WHERE id = 100;\n",[22,10805,10806],{"__ignoreMap":150},[203,10807,10808,10810,10812,10814,10817,10819,10821,10823,10825],{"class":205,"line":206},[203,10809,8365],{"class":209},[203,10811,8731],{"class":209},[203,10813,8734],{"class":209},[203,10815,10816],{"class":219}," users ",[203,10818,8740],{"class":209},[203,10820,9137],{"class":219},[203,10822,495],{"class":209},[203,10824,8749],{"class":718},[203,10826,1297],{"class":219},[18,10828,10829],{},"沿聚簇索引找到叶子记录时，通常已经取得了完整行数据。",[18,10831,10832],{},"这也是为什么官方文档把 InnoDB 表称为按聚簇索引组织的数据结构。",[14,10834,10836],{"id":10835},"十二innodb-二级索引如何组织数据","十二、InnoDB 二级索引如何组织数据",[18,10838,10839],{},"二级索引的叶子记录不直接保存完整行，而是保存：",[144,10841,10844],{"className":10842,"code":10843,"language":149,"meta":150},[147],"二级索引列 + 主键值\n",[22,10845,10843],{"__ignoreMap":150},[18,10847,10848],{},"假设存在：",[144,10850,10852],{"className":8679,"code":10851,"language":8681,"meta":150,"style":150},"CREATE INDEX idx_users_name ON users(name);\n",[22,10853,10854],{"__ignoreMap":150},[203,10855,10856,10858,10860,10863,10865,10868,10871],{"class":205,"line":206},[203,10857,9045],{"class":209},[203,10859,9048],{"class":209},[203,10861,10862],{"class":215}," idx_users_name",[203,10864,9054],{"class":209},[203,10866,10867],{"class":219}," users(",[203,10869,10870],{"class":209},"name",[203,10872,227],{"class":219},[18,10874,10875],{},"执行：",[144,10877,10879],{"className":8679,"code":10878,"language":8681,"meta":150,"style":150},"SELECT age\nFROM users\nWHERE name = 'Tom';\n",[22,10880,10881,10888,10895],{"__ignoreMap":150},[203,10882,10883,10885],{"class":205,"line":206},[203,10884,8365],{"class":209},[203,10886,10887],{"class":219}," age\n",[203,10889,10890,10892],{"class":205,"line":251},[203,10891,10537],{"class":209},[203,10893,10894],{"class":219}," users\n",[203,10896,10897,10899,10902,10904,10907],{"class":205,"line":260},[203,10898,8740],{"class":209},[203,10900,10901],{"class":209}," name",[203,10903,9514],{"class":209},[203,10905,10906],{"class":223}," 'Tom'",[203,10908,1297],{"class":219},[18,10910,4262,10911,10913],{},[22,10912,2562],{}," 不在二级索引中，查询过程通常是：",[144,10915,10918],{"className":10916,"code":10917,"language":149,"meta":150},[147],"idx_users_name B+ 树\n        ↓\n找到 name='Tom' 对应的主键\n        ↓\n回到聚簇索引 B+ 树\n        ↓\n根据主键取得完整行和 age\n",[22,10919,10917],{"__ignoreMap":150},[18,10921,10922],{},"这就是回表。",[18,10924,10925],{},"如果查询所需列都能从二级索引叶子记录中取得：",[144,10927,10929],{"className":8679,"code":10928,"language":8681,"meta":150,"style":150},"SELECT id\nFROM users\nWHERE name = 'Tom';\n",[22,10930,10931,10938,10944],{"__ignoreMap":150},[203,10932,10933,10935],{"class":205,"line":206},[203,10934,8365],{"class":209},[203,10936,10937],{"class":219}," id\n",[203,10939,10940,10942],{"class":205,"line":251},[203,10941,10537],{"class":209},[203,10943,10894],{"class":219},[203,10945,10946,10948,10950,10952,10954],{"class":205,"line":260},[203,10947,8740],{"class":209},[203,10949,10901],{"class":209},[203,10951,9514],{"class":209},[203,10953,10906],{"class":223},[203,10955,1297],{"class":219},[18,10957,10958],{},"那么可能直接通过二级索引完成查询，不需要回到聚簇索引，这就是覆盖索引。",[18,10960,10961],{},[119,10962,10965],{"href":10963,"target":122,"rel":10964},"\u002Fimages\u002Fwiki\u002Fjava\u002Fmysql-innodb-index-lookup.svg",[124,125],[127,10966],{"src":10963,"alt":10967},"InnoDB 聚簇索引、二级索引与回表",[14,10969,10971],{"id":10970},"十三为什么主键不宜过长","十三、为什么主键不宜过长",[18,10973,10974],{},"InnoDB 的每条二级索引记录都包含主键值。",[18,10976,10977],{},"如果主键很长：",[144,10979,10982],{"className":10980,"code":10981,"language":149,"meta":150},[147],"每条二级索引记录更大\n        ↓\n一个索引页容纳的记录更少\n        ↓\n索引占用空间增加\n        ↓\n缓存命中率和扇出可能下降\n",[22,10983,10981],{"__ignoreMap":150},[18,10985,10986],{},"因此，在满足业务要求的前提下，较短且稳定的主键通常更有利于 InnoDB 索引组织。",[18,10988,10989],{},"这并不意味着所有表都必须使用自增整数主键。是否采用自增 ID、分布式 ID 或业务主键，还要考虑写入热点、分库分表、数据合并和业务语义。",[14,10991,10993],{"id":10992},"十四叶子节点有序带来的其他收益","十四、叶子节点有序带来的其他收益",[18,10995,10996],{},"除了范围查询，叶子节点按键值有序还可以帮助：",[159,10998,10999,11002,11005,11008,11011],{},[162,11000,11001],{},"按索引顺序输出数据，减少额外排序；",[162,11003,11004],{},"快速定位最小值和最大值；",[162,11006,11007],{},"连续读取相邻索引记录；",[162,11009,11010],{},"对联合索引执行最左前缀扫描；",[162,11012,11013],{},"利用页预读和 Buffer Pool 提高连续访问效率。",[18,11015,11016],{},"但“逻辑有序”不等于“所有叶子页在磁盘上永远物理连续”。页面分裂、删除、合并和长期更新都会产生碎片，同层索引页主要依靠页面头中保存的前后页页号维持逻辑顺序。这些页号发挥逻辑指针的作用，并不是进程内存地址意义上的裸指针。",[14,11018,11020],{"id":11019},"十五b-树的写入代价","十五、B+ 树的写入代价",[18,11022,11023],{},"B+ 树并不是只有优点。",[18,11025,11026],{},"插入新记录时，需要先找到目标叶子页。如果页面空间不足，可能发生页分裂：",[144,11028,11031],{"className":11029,"code":11030,"language":149,"meta":150},[147],"一个满页\n   ↓\n分裂为两个页\n   ↓\n更新父节点目录项\n",[22,11032,11030],{"__ignoreMap":150},[18,11034,11035],{},"删除大量记录后，也可能触发页面合并或树结构收缩。",[18,11037,11038],{},"随机主键写入容易把数据分散到不同叶子页，可能带来：",[159,11040,11041,11044,11047,11050],{},[162,11042,11043],{},"更多随机页面访问；",[162,11045,11046],{},"页分裂；",[162,11048,11049],{},"页面利用率下降；",[162,11051,11052],{},"Buffer Pool 压力增加。",[18,11054,11055],{},"顺序递增键通常更容易写入索引右侧页面，但高并发场景下也可能形成右侧热点。主键选择需要结合实际写入模式评估。",[14,11057,11059],{"id":11058},"十六常见误区","十六、常见误区",[473,11061,11063],{"id":11062},"误区-1mysql-所有索引都是-b-树","误区 1：MySQL 所有索引都是 B+ 树",[18,11065,11066],{},"不准确。这里主要讨论 InnoDB 的普通索引。空间索引、全文索引和内部自适应哈希索引采用不同结构或机制。",[473,11068,11070],{"id":11069},"误区-2b-树的所有数据都只存在叶子节点","误区 2：B+ 树的所有数据都只存在叶子节点",[18,11072,11073],{},"需要区分“完整索引记录”和“分隔键”。非叶子节点也会保存用于导航的键，但完整索引记录集中在叶子节点。",[473,11075,11077],{"id":11076},"误区-3b-树一定比-b-树快","误区 3：B+ 树一定比 B 树快",[18,11079,11080],{},"不一定。纯内存、单次等值查询中，B 树可能提前在非叶子节点命中。B+ 树的优势主要体现在页面扇出、稳定查询路径和范围扫描。",[473,11082,11084],{"id":11083},"误区-4使用-b-树后查询一定只需要三次-io","误区 4：使用 B+ 树后查询一定只需要三次 I\u002FO",[18,11086,11087],{},"树高不是固定值，取决于页面大小、键长度、记录大小、填充率和数据量。页面还可能已经位于 Buffer Pool 中，因此树层访问次数也不能直接等同于磁盘 I\u002FO 次数。",[473,11089,11091],{"id":11090},"误区-5建立索引就一定能提高性能","误区 5：建立索引就一定能提高性能",[18,11093,11094,11095,53,11098,53,11100,11102],{},"索引会占用空间并增加 ",[22,11096,11097],{},"INSERT",[22,11099,8344],{},[22,11101,8347],{}," 的维护成本。低选择性索引、无法满足最左前缀的查询或返回大量数据的查询，也可能不使用索引。",[14,11104,11106],{"id":11105},"十七回答时可以怎样组织","十七、回答时可以怎样组织",[18,11108,11109],{},"面试时可以按照以下顺序回答：",[144,11111,11114],{"className":11112,"code":11113,"language":149,"meta":150},[147],"先说结构差异\n    ↓\n再说数据库按页读取\n    ↓\n解释扇出更大、树高更低\n    ↓\n解释叶子节点有序，适合范围扫描\n    ↓\n结合 InnoDB 聚簇索引和二级索引\n",[22,11115,11113],{"__ignoreMap":150},[18,11117,11118,11119,11121],{},"重点不是只说“B+ 树查询复杂度是 ",[22,11120,10403],{},"”，而是说明：",[105,11123,11124],{},[18,11125,11126],{},"InnoDB 通过页组织索引，B+ 树让一个非叶子页保存更多导航项，从而用较少层级管理大量数据；定位到叶子页后，又能沿有序叶子页完成范围扫描。",[14,11128,5803],{"id":5803},[159,11130,11131,11138,11145,11152,11159],{},[162,11132,11133],{},[119,11134,11137],{"href":11135,"rel":11136},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-index-types.html",[2080],"MySQL 8.4 Reference Manual：Clustered and Secondary Indexes",[162,11139,11140],{},[119,11141,11144],{"href":11142,"rel":11143},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-physical-structure.html",[2080],"MySQL 8.4 Reference Manual：The Physical Structure of an InnoDB Index",[162,11146,11147],{},[119,11148,11151],{"href":11149,"rel":11150},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Fcolumn-indexes.html",[2080],"MySQL 8.4 Reference Manual：Column Indexes",[162,11153,11154],{},[119,11155,11158],{"href":11156,"rel":11157},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Findex-btree-hash.html",[2080],"MySQL 8.4 Reference Manual：Comparison of B-Tree and Hash Indexes",[162,11160,11161],{},[119,11162,11165],{"href":11163,"rel":11164},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-file-space.html",[2080],"MySQL 8.4 Reference Manual：File Space Management",[2111,11167,11168],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":150,"searchDepth":251,"depth":251,"links":11170},[11171,11174,11175,11176,11177,11178,11179,11180,11181,11182,11183,11184,11185,11186,11187,11188,11189,11190,11191,11198,11199],{"id":16,"depth":251,"text":16,"children":11172},[11173],{"id":8377,"depth":260,"text":8377},{"id":115,"depth":251,"text":115},{"id":132,"depth":251,"text":132},{"id":10116,"depth":251,"text":10117},{"id":10166,"depth":251,"text":10167},{"id":10212,"depth":251,"text":10213},{"id":10264,"depth":251,"text":10265},{"id":10378,"depth":251,"text":10379},{"id":10412,"depth":251,"text":10413},{"id":10459,"depth":251,"text":10460},{"id":10516,"depth":251,"text":10517},{"id":10633,"depth":251,"text":10634},{"id":10659,"depth":251,"text":10660},{"id":10766,"depth":251,"text":10767},{"id":10835,"depth":251,"text":10836},{"id":10970,"depth":251,"text":10971},{"id":10992,"depth":251,"text":10993},{"id":11019,"depth":251,"text":11020},{"id":11058,"depth":251,"text":11059,"children":11192},[11193,11194,11195,11196,11197],{"id":11062,"depth":260,"text":11063},{"id":11069,"depth":260,"text":11070},{"id":11076,"depth":260,"text":11077},{"id":11083,"depth":260,"text":11084},{"id":11090,"depth":260,"text":11091},{"id":11105,"depth":251,"text":11106},{"id":5803,"depth":251,"text":5803},"wiki:java:mysql-bplus-tree-vs-btree","从磁盘页、树高、扇出、范围查询和 InnoDB 索引组织方式出发，解释 MySQL 为何采用 B+ 树，并对比 B 树、红黑树与哈希索引。",{},76,"\u002Fjava\u002Fmysql-bplus-tree-vs-btree",{"title":10063,"description":11201},"java\u002Fmysql-bplus-tree-vs-btree","2026-07-25","jndYWwP27t-baJwZJsdJfVBgvgmQyLB_Z7vIQtvgXow",{"id":11210,"title":11211,"body":11212,"commentId":15943,"description":15944,"difficulty":3667,"draft":2148,"extension":2149,"meta":15945,"navigation":756,"order":15946,"path":15947,"section":2153,"seo":15948,"stem":15949,"updated":11207,"__hash__":15950},"java\u002Fjava\u002Fmysql-index-failure-scenarios.md","MySQL 索引在什么情况下会失效？",{"type":7,"value":11213,"toc":15876},[11214,11217,11219,11222,11252,11255,11298,11320,11322,11327,11329,11338,11340,11344,11347,11351,11354,11357,11370,11373,11404,11407,11411,11414,11416,11428,11431,11487,11502,11511,11515,11518,11524,11527,11530,11534,11537,11547,11564,11567,11573,11576,11657,11670,11673,11716,11719,11722,11726,11729,11734,11737,11740,11750,11752,11784,11787,11811,11814,11824,11828,11834,11846,11849,11883,11893,11896,11937,11940,11996,11999,12015,12018,12021,12033,12038,12044,12047,12067,12082,12085,12101,12104,12131,12134,12152,12155,12200,12202,12216,12220,12223,12251,12254,12271,12274,12290,12293,12296,12310,12313,12316,12321,12325,12327,12344,12347,12364,12371,12374,12401,12404,12407,12418,12428,12432,12439,12481,12488,12491,12511,12516,12520,12527,12530,12554,12556,12595,12598,12604,12610,12616,12619,12683,12689,12699,12741,12744,12750,12755,12762,12776,12785,12788,12811,12814,12818,12825,12835,12837,12862,12865,12881,12884,12887,12891,12894,12944,12947,12952,12955,12961,12964,12970,12974,12977,13006,13011,13017,13020,13023,13047,13051,13057,13060,13070,13072,13106,13115,13118,13141,13146,13149,13166,13169,13171,13181,13184,13228,13237,13243,13248,13281,13292,13298,13311,13314,13357,13372,13375,13378,13465,13479,13485,13496,13509,13512,13515,13518,13540,13543,13576,13579,13582,13599,13606,13626,13629,13646,13649,13654,13660,13672,13678,13682,13685,13699,13702,13705,13721,13724,13727,13731,13734,13768,13771,13863,13870,13882,13885,13890,13893,13899,13902,13904,13930,13940,13948,13955,13958,13964,13970,13976,13981,13984,14048,14051,14080,14083,14114,14117,14123,14128,14143,14170,14173,14188,14193,14198,14203,14211,14216,14223,14225,14275,14328,14339,14345,14352,14359,14403,14409,14414,14416,14431,14459,14465,14471,14477,14482,14498,14512,14515,14520,14563,14578,14585,14590,14593,14633,14636,14642,14648,14686,14688,14716,14724,14727,14753,14756,14762,14771,14777,14785,14788,14807,14812,14818,14824,14828,14831,14834,14844,14846,14894,14902,14905,14911,14919,14925,14928,14933,14940,14946,14949,14963,14966,14973,14978,14985,14990,14996,15008,15010,15021,15039,15049,15052,15058,15063,15066,15089,15097,15105,15112,15117,15120,15126,15129,15150,15152,15182,15187,15193,15196,15215,15221,15225,15362,15367,15372,15376,15383,15415,15425,15430,15464,15467,15473,15476,15508,15511,15586,15591,15597,15600,15603,15617,15624,15627,15633,15642,15645,15651,15654,15657,15663,15668,15671,15702,15706,15709,15756,15760,15764,15767,15771,15774,15784,15787,15793,15796,15800,15803,15810,15813,15815,15873],[10,11215,11211],{"id":11216},"mysql-索引在什么情况下会失效",[14,11218,16],{"id":16},[18,11220,11221],{},"所谓“索引失效”并不是 MySQL 的正式术语，实际需要区分三种情况：",[2695,11223,11224,11230,11249],{},[162,11225,11226,11227,11229],{},"SQL 写法或索引结构决定了索引无法用于快速定位，例如联合索引没有满足最左前缀、查询的是索引列的计算结果但索引只保存原始列值、",[22,11228,10729],{}," 以通配符开头。",[162,11231,11232,11233,11236,11237,11240,11241,11244,11245,11248],{},"索引可以使用，但只能使用其中一部分。例如联合索引 ",[22,11234,11235],{},"(a,b,c)"," 查询 ",[22,11238,11239],{},"a=1 AND b>10 AND c=3"," 时，通常可以用 ",[22,11242,11243],{},"a、b"," 确定扫描范围，",[22,11246,11247],{},"c"," 可能通过索引条件下推继续过滤，却不能进一步缩小连续的索引扫描区间。",[162,11250,11251],{},"索引具备使用条件，但优化器估算全表扫描成本更低，于是主动不选。例如查询返回大量数据、索引选择性很低或回表成本过高。",[18,11253,11254],{},"常见场景包括：",[159,11256,11257,11260,11263,11266,11275,11281,11292,11295],{},[162,11258,11259],{},"联合索引不满足最左前缀；",[162,11261,11262],{},"在索引列上执行函数、计算或不匹配的表达式；",[162,11264,11265],{},"比较两侧数据类型、字符集或排序规则不兼容，引发隐式转换；",[162,11267,11268,6265,11271,11274],{},[22,11269,11270],{},"LIKE '%keyword'",[22,11272,11273],{},"LIKE '%keyword%'"," 以通配符开头；",[162,11276,11277,11280],{},[22,11278,11279],{},"OR"," 的某个分支缺少可用索引，且优化器无法采用合适的 Index Merge；",[162,11282,11283,53,11285,53,11288,11291],{},[22,11284,1350],{},[22,11286,11287],{},"NOT IN",[22,11289,11290],{},"IS NOT NULL"," 等条件命中数据过多；",[162,11293,11294],{},"查询需要返回表中很大比例的数据，优化器认为全表扫描更便宜；",[162,11296,11297],{},"索引统计信息不准确，导致优化器成本估算偏差。",[18,11299,11300,11301,11304,11305,6265,11308,11311,11312,11315,11316,11319],{},"判断索引是否真正有效，不能只看 ",[22,11302,11303],{},"possible_keys","，应使用 ",[22,11306,11307],{},"EXPLAIN",[22,11309,11310],{},"EXPLAIN ANALYZE","，重点观察实际选择的 ",[22,11313,11314],{},"key","、访问类型 ",[22,11317,11318],{},"type","、预计或实际扫描行数，以及是否发生大量回表。",[473,11321,8377],{"id":8377},[105,11323,11324],{},[18,11325,11326],{},"索引失效要区分“不能用、只用一部分、能用但优化器不选”；排查时先看索引结构和 SQL 写法，再用执行计划验证实际扫描成本。",[14,11328,115],{"id":115},[18,11330,11331],{},[119,11332,11335],{"href":11333,"target":122,"rel":11334},"\u002Fimages\u002Fwiki\u002Fjava\u002Fmysql-index-failure-scenarios.svg",[124,125],[127,11336],{"src":11333,"alt":11337},"MySQL 索引失效的三类情况与排查路径",[14,11339,132],{"id":132},[14,11341,11343],{"id":11342},"一先区分三种索引失效","一、先区分三种“索引失效”",[18,11345,11346],{},"日常讨论经常把所有没有达到预期性能的情况都叫作“索引失效”，但这会掩盖真正的问题。",[473,11348,11350],{"id":11349},"_1-索引无法用于定位","1. 索引无法用于定位",[18,11352,11353],{},"索引的有序结构与查询条件不匹配，MySQL 无法根据该条件确定一个有效的索引查找范围。",[18,11355,11356],{},"例如索引为：",[144,11358,11360],{"className":8679,"code":11359,"language":8681,"meta":150,"style":150},"KEY idx_user_time (user_id, created_at)\n",[22,11361,11362],{"__ignoreMap":150},[203,11363,11364,11367],{"class":205,"line":206},[203,11365,11366],{"class":209},"KEY",[203,11368,11369],{"class":219}," idx_user_time (user_id, created_at)\n",[18,11371,11372],{},"查询只使用第二列：",[144,11374,11376],{"className":8679,"code":11375,"language":8681,"meta":150,"style":150},"SELECT *\nFROM orders\nWHERE created_at >= '2026-07-01';\n",[22,11377,11378,11384,11390],{"__ignoreMap":150},[203,11379,11380,11382],{"class":205,"line":206},[203,11381,8365],{"class":209},[203,11383,10532],{"class":209},[203,11385,11386,11388],{"class":205,"line":251},[203,11387,10537],{"class":209},[203,11389,8862],{"class":219},[203,11391,11392,11394,11397,11399,11402],{"class":205,"line":260},[203,11393,8740],{"class":209},[203,11395,11396],{"class":219}," created_at ",[203,11398,10594],{"class":209},[203,11400,11401],{"class":223}," '2026-07-01'",[203,11403,1297],{"class":219},[18,11405,11406],{},"传统的联合索引范围查找无法绕过第一列，直接按照第二列定位连续区间。",[473,11408,11410],{"id":11409},"_2-只使用了部分索引能力","2. 只使用了部分索引能力",[18,11412,11413],{},"SQL 使用了索引，但索引没有把扫描范围缩小到预期程度。",[18,11415,686],{},[144,11417,11419],{"className":8679,"code":11418,"language":8681,"meta":150,"style":150},"KEY idx_abc (a, b, c)\n",[22,11420,11421],{"__ignoreMap":150},[203,11422,11423,11425],{"class":205,"line":206},[203,11424,11366],{"class":209},[203,11426,11427],{"class":219}," idx_abc (a, b, c)\n",[18,11429,11430],{},"查询：",[144,11432,11434],{"className":8679,"code":11433,"language":8681,"meta":150,"style":150},"SELECT *\nFROM t\nWHERE a = 1\n  AND b > 10\n  AND c = 3;\n",[22,11435,11436,11442,11449,11461,11473],{"__ignoreMap":150},[203,11437,11438,11440],{"class":205,"line":206},[203,11439,8365],{"class":209},[203,11441,10532],{"class":209},[203,11443,11444,11446],{"class":205,"line":251},[203,11445,10537],{"class":209},[203,11447,11448],{"class":219}," t\n",[203,11450,11451,11453,11456,11458],{"class":205,"line":260},[203,11452,8740],{"class":209},[203,11454,11455],{"class":219}," a ",[203,11457,495],{"class":209},[203,11459,11460],{"class":718}," 1\n",[203,11462,11463,11465,11468,11470],{"class":205,"line":278},[203,11464,9508],{"class":209},[203,11466,11467],{"class":219}," b ",[203,11469,9371],{"class":209},[203,11471,11472],{"class":718}," 10\n",[203,11474,11475,11477,11480,11482,11485],{"class":205,"line":285},[203,11476,9508],{"class":209},[203,11478,11479],{"class":219}," c ",[203,11481,495],{"class":209},[203,11483,11484],{"class":718}," 3",[203,11486,1297],{"class":219},[18,11488,11489,11490,11493,11494,11497,11498,11501],{},"通常可以使用 ",[22,11491,11492],{},"a=1 AND b>10"," 构造索引扫描范围。由于 ",[22,11495,11496],{},"b"," 是范围条件，",[22,11499,11500],{},"c=3"," 一般不能继续把这个范围切成更小的连续区间。",[18,11503,11504,11505,11507,11508,11510],{},"但这不等于 ",[22,11506,11247],{}," 完全没用。满足条件时，MySQL 可以通过索引条件下推（Index Condition Pushdown，ICP），在索引层先判断 ",[22,11509,11500],{},"，减少回表数量。",[473,11512,11514],{"id":11513},"_3-优化器主动不选择索引","3. 优化器主动不选择索引",[18,11516,11517],{},"索引本身可用，但 MySQL 优化器根据统计信息估算：",[144,11519,11522],{"className":11520,"code":11521,"language":149,"meta":150},[147],"使用二级索引扫描 + 大量回表\n                >\n顺序扫描整张表\n",[22,11523,11521],{"__ignoreMap":150},[18,11525,11526],{},"于是执行计划选择全表扫描。",[18,11528,11529],{},"这种情况不是索引结构失效，而是成本选择。强行使用索引不一定更快。",[14,11531,11533],{"id":11532},"二联合索引不满足最左前缀","二、联合索引不满足最左前缀",[18,11535,11536],{},"假设存在联合索引：",[144,11538,11539],{"className":8679,"code":11418,"language":8681,"meta":150,"style":150},[22,11540,11541],{"__ignoreMap":150},[203,11542,11543,11545],{"class":205,"line":206},[203,11544,11366],{"class":209},[203,11546,11427],{"class":219},[18,11548,11549,11550,11552,11553,11555,11556,11552,11558,11560,11561,11563],{},"B+ 树中的记录首先按 ",[22,11551,119],{}," 排序；",[22,11554,119],{}," 相同时，再按 ",[22,11557,11496],{},[22,11559,11243],{}," 都相同时，才按 ",[22,11562,11247],{}," 排序。",[18,11565,11566],{},"可以近似理解为：",[144,11568,11571],{"className":11569,"code":11570,"language":149,"meta":150},[147],"(a=1, b=1, c=1)\n(a=1, b=1, c=5)\n(a=1, b=2, c=2)\n(a=2, b=1, c=3)\n",[22,11572,11570],{"__ignoreMap":150},[18,11574,11575],{},"因此以下条件通常能够使用联合索引定位：",[144,11577,11579],{"className":8679,"code":11578,"language":8681,"meta":150,"style":150},"WHERE a = 1\nWHERE a = 1 AND b = 2\nWHERE a = 1 AND b = 2 AND c = 3\nWHERE a = 1 AND c = 3\n",[22,11580,11581,11591,11611,11639],{"__ignoreMap":150},[203,11582,11583,11585,11587,11589],{"class":205,"line":206},[203,11584,8740],{"class":209},[203,11586,11455],{"class":219},[203,11588,495],{"class":209},[203,11590,11460],{"class":718},[203,11592,11593,11595,11597,11599,11602,11604,11606,11608],{"class":205,"line":251},[203,11594,8740],{"class":209},[203,11596,11455],{"class":219},[203,11598,495],{"class":209},[203,11600,11601],{"class":718}," 1",[203,11603,8752],{"class":209},[203,11605,11467],{"class":219},[203,11607,495],{"class":209},[203,11609,11610],{"class":718}," 2\n",[203,11612,11613,11615,11617,11619,11621,11623,11625,11627,11630,11632,11634,11636],{"class":205,"line":260},[203,11614,8740],{"class":209},[203,11616,11455],{"class":219},[203,11618,495],{"class":209},[203,11620,11601],{"class":718},[203,11622,8752],{"class":209},[203,11624,11467],{"class":219},[203,11626,495],{"class":209},[203,11628,11629],{"class":718}," 2",[203,11631,8752],{"class":209},[203,11633,11479],{"class":219},[203,11635,495],{"class":209},[203,11637,11638],{"class":718}," 3\n",[203,11640,11641,11643,11645,11647,11649,11651,11653,11655],{"class":205,"line":278},[203,11642,8740],{"class":209},[203,11644,11455],{"class":219},[203,11646,495],{"class":209},[203,11648,11601],{"class":718},[203,11650,8752],{"class":209},[203,11652,11479],{"class":219},[203,11654,495],{"class":209},[203,11656,11638],{"class":718},[18,11658,11659,11660,11663,11664,11666,11667,11669],{},"最后一个条件仍然可以通过 ",[22,11661,11662],{},"a=1"," 定位范围，只是缺少 ",[22,11665,11496],{}," 后，",[22,11668,11500],{}," 通常不能继续缩小查找区间。",[18,11671,11672],{},"以下查询没有使用最左列：",[144,11674,11676],{"className":8679,"code":11675,"language":8681,"meta":150,"style":150},"WHERE b = 2\nWHERE c = 3\nWHERE b = 2 AND c = 3\n",[22,11677,11678,11688,11698],{"__ignoreMap":150},[203,11679,11680,11682,11684,11686],{"class":205,"line":206},[203,11681,8740],{"class":209},[203,11683,11467],{"class":219},[203,11685,495],{"class":209},[203,11687,11610],{"class":718},[203,11689,11690,11692,11694,11696],{"class":205,"line":251},[203,11691,8740],{"class":209},[203,11693,11479],{"class":219},[203,11695,495],{"class":209},[203,11697,11638],{"class":718},[203,11699,11700,11702,11704,11706,11708,11710,11712,11714],{"class":205,"line":260},[203,11701,8740],{"class":209},[203,11703,11467],{"class":219},[203,11705,495],{"class":209},[203,11707,11629],{"class":718},[203,11709,8752],{"class":209},[203,11711,11479],{"class":219},[203,11713,495],{"class":209},[203,11715,11638],{"class":718},[18,11717,11718],{},"它们通常无法使用传统的最左前缀方式直接定位。",[18,11720,11721],{},"需要注意，MySQL 8.0 在满足条件时可能使用 Skip Scan，把缺失的最左列按多个可能值分别扫描。因此“缺少最左列一定完全不用索引”也不是绝对结论，最终必须以执行计划为准。",[14,11723,11725],{"id":11724},"三范围条件之后的列还能不能使用","三、范围条件之后的列还能不能使用",[18,11727,11728],{},"常见说法是：",[105,11730,11731],{},[18,11732,11733],{},"联合索引遇到范围查询后，后面的列全部失效。",[18,11735,11736],{},"这个说法过于绝对。",[18,11738,11739],{},"索引为：",[144,11741,11742],{"className":8679,"code":11418,"language":8681,"meta":150,"style":150},[22,11743,11744],{"__ignoreMap":150},[203,11745,11746,11748],{"class":205,"line":206},[203,11747,11366],{"class":209},[203,11749,11427],{"class":219},[18,11751,11430],{},[144,11753,11755],{"className":8679,"code":11754,"language":8681,"meta":150,"style":150},"WHERE a = 1 AND b > 10 AND c = 3\n",[22,11756,11757],{"__ignoreMap":150},[203,11758,11759,11761,11763,11765,11767,11769,11771,11773,11776,11778,11780,11782],{"class":205,"line":206},[203,11760,8740],{"class":209},[203,11762,11455],{"class":219},[203,11764,495],{"class":209},[203,11766,11601],{"class":718},[203,11768,8752],{"class":209},[203,11770,11467],{"class":219},[203,11772,9371],{"class":209},[203,11774,11775],{"class":718}," 10",[203,11777,8752],{"class":209},[203,11779,11479],{"class":219},[203,11781,495],{"class":209},[203,11783,11638],{"class":718},[18,11785,11786],{},"更准确的理解是：",[159,11788,11789,11797,11802,11808],{},[162,11790,11791,40,11793,11796],{},[22,11792,11662],{},[22,11794,11795],{},"b>10"," 可以共同确定扫描范围；",[162,11798,11799,11801],{},[22,11800,11500],{}," 通常不能继续缩小这个连续范围；",[162,11803,11804,11805,11807],{},"如果启用了 ICP，",[22,11806,11500],{}," 仍可能在索引层参与过滤；",[162,11809,11810],{},"如果查询列都包含在索引中，还可能形成覆盖索引，避免回表。",[18,11812,11813],{},"所以“能否参与范围定位”“能否参与索引层过滤”和“能否避免回表”是三个不同问题。",[18,11815,11816,11817,11820,11821,11823],{},"另外，",[22,11818,11819],{},"IN"," 在很多情况下会被转换成多个等值范围，不应简单地把所有 ",[22,11822,11819],{}," 都视为遇到范围后索引失效。",[14,11825,11827],{"id":11826},"四在索引列上进行函数或计算","四、在索引列上进行函数或计算",[18,11829,9031,11830,11833],{},[22,11831,11832],{},"created_at"," 上有普通索引：",[144,11835,11837],{"className":8679,"code":11836,"language":8681,"meta":150,"style":150},"KEY idx_created_at (created_at)\n",[22,11838,11839],{"__ignoreMap":150},[203,11840,11841,11843],{"class":205,"line":206},[203,11842,11366],{"class":209},[203,11844,11845],{"class":219}," idx_created_at (created_at)\n",[18,11847,11848],{},"下面的写法把函数作用在索引列上：",[144,11850,11852],{"className":8679,"code":11851,"language":8681,"meta":150,"style":150},"SELECT *\nFROM orders\nWHERE DATE(created_at) = '2026-07-25';\n",[22,11853,11854,11860,11866],{"__ignoreMap":150},[203,11855,11856,11858],{"class":205,"line":206},[203,11857,8365],{"class":209},[203,11859,10532],{"class":209},[203,11861,11862,11864],{"class":205,"line":251},[203,11863,10537],{"class":209},[203,11865,8862],{"class":219},[203,11867,11868,11870,11873,11876,11878,11881],{"class":205,"line":260},[203,11869,8740],{"class":209},[203,11871,11872],{"class":209}," DATE",[203,11874,11875],{"class":219},"(created_at) ",[203,11877,495],{"class":209},[203,11879,11880],{"class":223}," '2026-07-25'",[203,11882,1297],{"class":219},[18,11884,11885,11886,11888,11889,11892],{},"普通索引保存的是原始 ",[22,11887,11832],{}," 值，而不是 ",[22,11890,11891],{},"DATE(created_at)"," 的计算结果。MySQL 通常无法直接根据普通索引定位函数结果。",[18,11894,11895],{},"可以改写为范围条件：",[144,11897,11899],{"className":8679,"code":11898,"language":8681,"meta":150,"style":150},"SELECT *\nFROM orders\nWHERE created_at >= '2026-07-25 00:00:00'\n  AND created_at \u003C  '2026-07-26 00:00:00';\n",[22,11900,11901,11907,11913,11924],{"__ignoreMap":150},[203,11902,11903,11905],{"class":205,"line":206},[203,11904,8365],{"class":209},[203,11906,10532],{"class":209},[203,11908,11909,11911],{"class":205,"line":251},[203,11910,10537],{"class":209},[203,11912,8862],{"class":219},[203,11914,11915,11917,11919,11921],{"class":205,"line":260},[203,11916,8740],{"class":209},[203,11918,11396],{"class":219},[203,11920,10594],{"class":209},[203,11922,11923],{"class":223}," '2026-07-25 00:00:00'\n",[203,11925,11926,11928,11930,11932,11935],{"class":205,"line":278},[203,11927,9508],{"class":209},[203,11929,11396],{"class":219},[203,11931,3306],{"class":209},[203,11933,11934],{"class":223},"  '2026-07-26 00:00:00'",[203,11936,1297],{"class":219},[18,11938,11939],{},"类似问题还包括：",[144,11941,11943],{"className":8679,"code":11942,"language":8681,"meta":150,"style":150},"WHERE amount + 1 = 100\nWHERE LOWER(username) = 'tom'\nWHERE CAST(order_no AS UNSIGNED) = 1001\n",[22,11944,11945,11960,11975],{"__ignoreMap":150},[203,11946,11947,11949,11951,11954,11956,11958],{"class":205,"line":206},[203,11948,8740],{"class":209},[203,11950,8743],{"class":219},[203,11952,11953],{"class":209},"+",[203,11955,11601],{"class":718},[203,11957,9514],{"class":209},[203,11959,9142],{"class":718},[203,11961,11962,11964,11967,11970,11972],{"class":205,"line":251},[203,11963,8740],{"class":209},[203,11965,11966],{"class":718}," LOWER",[203,11968,11969],{"class":219},"(username) ",[203,11971,495],{"class":209},[203,11973,11974],{"class":223}," 'tom'\n",[203,11976,11977,11979,11982,11985,11988,11991,11993],{"class":205,"line":260},[203,11978,8740],{"class":209},[203,11980,11981],{"class":718}," CAST",[203,11983,11984],{"class":219},"(order_no ",[203,11986,11987],{"class":209},"AS",[203,11989,11990],{"class":219}," UNSIGNED) ",[203,11992,495],{"class":209},[203,11994,11995],{"class":718}," 1001\n",[18,11997,11998],{},"应优先把运算移动到常量一侧：",[144,12000,12002],{"className":8679,"code":12001,"language":8681,"meta":150,"style":150},"WHERE amount = 99\n",[22,12003,12004],{"__ignoreMap":150},[203,12005,12006,12008,12010,12012],{"class":205,"line":206},[203,12007,8740],{"class":209},[203,12009,8743],{"class":219},[203,12011,495],{"class":209},[203,12013,12014],{"class":718}," 99\n",[18,12016,12017],{},"这里所说的“对索引列进行未建立函数索引的运算”，可以用下面的例子理解。",[18,12019,12020],{},"普通索引：",[144,12022,12024],{"className":8679,"code":12023,"language":8681,"meta":150,"style":150},"KEY idx_amount (amount)\n",[22,12025,12026],{"__ignoreMap":150},[203,12027,12028,12030],{"class":205,"line":206},[203,12029,11366],{"class":209},[203,12031,12032],{"class":219}," idx_amount (amount)\n",[18,12034,12035,12036,431],{},"保存并排序的是原始 ",[22,12037,9034],{},[144,12039,12042],{"className":12040,"code":12041,"language":149,"meta":150},[147],"10 → 20 → 30 → 40\n",[22,12043,12041],{"__ignoreMap":150},[18,12045,12046],{},"查询条件却是：",[144,12048,12050],{"className":8679,"code":12049,"language":8681,"meta":150,"style":150},"WHERE amount + 1 = 31\n",[22,12051,12052],{"__ignoreMap":150},[203,12053,12054,12056,12058,12060,12062,12064],{"class":205,"line":206},[203,12055,8740],{"class":209},[203,12057,8743],{"class":219},[203,12059,11953],{"class":209},[203,12061,11601],{"class":718},[203,12063,9514],{"class":209},[203,12065,12066],{"class":718}," 31\n",[18,12068,12069,12070,12073,12074,12077,12078,12081],{},"MySQL 需要比较的是 ",[22,12071,12072],{},"amount + 1"," 的结果，而普通索引中没有保存这个结果。它通常无法直接在 ",[22,12075,12076],{},"idx_amount"," 中查找 ",[22,12079,12080],{},"31","，而需要对候选记录计算表达式。",[18,12083,12084],{},"这个条件可以直接改写为：",[144,12086,12088],{"className":8679,"code":12087,"language":8681,"meta":150,"style":150},"WHERE amount = 30\n",[22,12089,12090],{"__ignoreMap":150},[203,12091,12092,12094,12096,12098],{"class":205,"line":206},[203,12093,8740],{"class":209},[203,12095,8743],{"class":219},[203,12097,495],{"class":209},[203,12099,12100],{"class":718}," 30\n",[18,12102,12103],{},"如果业务确实需要长期按照某个表达式查询，MySQL 8.0.13 及以上版本支持函数索引：",[144,12105,12107],{"className":8679,"code":12106,"language":8681,"meta":150,"style":150},"CREATE INDEX idx_amount_plus_one\nON orders ((amount + 1));\n",[22,12108,12109,12118],{"__ignoreMap":150},[203,12110,12111,12113,12115],{"class":205,"line":206},[203,12112,9045],{"class":209},[203,12114,9048],{"class":209},[203,12116,12117],{"class":215}," idx_amount_plus_one\n",[203,12119,12120,12122,12125,12127,12129],{"class":205,"line":251},[203,12121,9565],{"class":209},[203,12123,12124],{"class":219}," orders ((amount ",[203,12126,11953],{"class":209},[203,12128,11601],{"class":718},[203,12130,5205],{"class":219},[18,12132,12133],{},"之后与索引表达式匹配的查询才可以直接检索表达式结果：",[144,12135,12136],{"className":8679,"code":12049,"language":8681,"meta":150,"style":150},[22,12137,12138],{"__ignoreMap":150},[203,12139,12140,12142,12144,12146,12148,12150],{"class":205,"line":206},[203,12141,8740],{"class":209},[203,12143,8743],{"class":219},[203,12145,11953],{"class":209},[203,12147,11601],{"class":718},[203,12149,9514],{"class":209},[203,12151,12066],{"class":718},[18,12153,12154],{},"也可以创建生成列，再为生成列建立索引：",[144,12156,12158],{"className":8679,"code":12157,"language":8681,"meta":150,"style":150},"ALTER TABLE orders\nADD COLUMN order_date DATE AS (DATE(created_at)),\nADD INDEX idx_order_date (order_date);\n",[22,12159,12160,12170,12191],{"__ignoreMap":150},[203,12161,12162,12165,12168],{"class":205,"line":206},[203,12163,12164],{"class":209},"ALTER",[203,12166,12167],{"class":209}," TABLE",[203,12169,8862],{"class":219},[203,12171,12172,12175,12178,12181,12184,12186,12188],{"class":205,"line":251},[203,12173,12174],{"class":209},"ADD",[203,12176,12177],{"class":219}," COLUMN order_date ",[203,12179,12180],{"class":209},"DATE",[203,12182,12183],{"class":209}," AS",[203,12185,5163],{"class":219},[203,12187,12180],{"class":209},[203,12189,12190],{"class":219},"(created_at)),\n",[203,12192,12193,12195,12197],{"class":205,"line":260},[203,12194,12174],{"class":209},[203,12196,9048],{"class":209},[203,12198,12199],{"class":219}," idx_order_date (order_date);\n",[18,12201,4831],{},[159,12203,12204,12207,12210,12213],{},[162,12205,12206],{},"函数索引保存的是表达式计算结果，不是让任意函数都自动使用原列索引；",[162,12208,12209],{},"查询中的表达式需要与索引定义匹配；",[162,12211,12212],{},"函数索引会占用空间，也会增加写入和更新成本；",[162,12214,12215],{},"能通过等价范围条件改写的 SQL，通常优先改写，未必需要新增索引。",[14,12217,12219],{"id":12218},"五隐式类型转换","五、隐式类型转换",[18,12221,12222],{},"假设订单号是字符串：",[144,12224,12226],{"className":8679,"code":12225,"language":8681,"meta":150,"style":150},"order_no VARCHAR(32),\nKEY idx_order_no (order_no)\n",[22,12227,12228,12244],{"__ignoreMap":150},[203,12229,12230,12233,12236,12238,12241],{"class":205,"line":206},[203,12231,12232],{"class":219},"order_no ",[203,12234,12235],{"class":209},"VARCHAR",[203,12237,220],{"class":219},[203,12239,12240],{"class":718},"32",[203,12242,12243],{"class":219},"),\n",[203,12245,12246,12248],{"class":205,"line":251},[203,12247,11366],{"class":209},[203,12249,12250],{"class":219}," idx_order_no (order_no)\n",[18,12252,12253],{},"推荐使用相同类型进行比较：",[144,12255,12257],{"className":8679,"code":12256,"language":8681,"meta":150,"style":150},"WHERE order_no = '10001'\n",[22,12258,12259],{"__ignoreMap":150},[203,12260,12261,12263,12266,12268],{"class":205,"line":206},[203,12262,8740],{"class":209},[203,12264,12265],{"class":219}," order_no ",[203,12267,495],{"class":209},[203,12269,12270],{"class":223}," '10001'\n",[18,12272,12273],{},"如果写成：",[144,12275,12277],{"className":8679,"code":12276,"language":8681,"meta":150,"style":150},"WHERE order_no = 10001\n",[22,12278,12279],{"__ignoreMap":150},[203,12280,12281,12283,12285,12287],{"class":205,"line":206},[203,12282,8740],{"class":209},[203,12284,12265],{"class":219},[203,12286,495],{"class":209},[203,12288,12289],{"class":718}," 10001\n",[18,12291,12292],{},"MySQL 需要按照类型转换规则比较字符串与数字，可能对索引列执行转换，导致无法按原有字符串顺序高效定位。",[18,12294,12295],{},"排查时要检查：",[159,12297,12298,12301,12304,12307],{},[162,12299,12300],{},"数字列是否与字符串参数比较；",[162,12302,12303],{},"字符串列的字符集是否一致；",[162,12305,12306],{},"字符串列的排序规则是否兼容；",[162,12308,12309],{},"JOIN 两侧字段的类型、长度、字符集和排序规则是否匹配。",[18,12311,12312],{},"例如两个字符串列字符集不兼容时，即使两侧都有索引，也可能无法高效使用索引完成连接。",[18,12314,12315],{},"最稳妥的原则是：",[105,12317,12318],{},[18,12319,12320],{},"SQL 参数类型应与索引列类型一致，JOIN 字段的类型和字符规则也应保持一致。",[14,12322,12324],{"id":12323},"六like-以通配符开头","六、LIKE 以通配符开头",[18,12326,11739],{},[144,12328,12330],{"className":8679,"code":12329,"language":8681,"meta":150,"style":150},"KEY idx_name (name)\n",[22,12331,12332],{"__ignoreMap":150},[203,12333,12334,12336,12339,12341],{"class":205,"line":206},[203,12335,11366],{"class":209},[203,12337,12338],{"class":219}," idx_name (",[203,12340,10870],{"class":209},[203,12342,12343],{"class":219},")\n",[18,12345,12346],{},"以下前缀匹配通常可以利用 B+ 树的有序性：",[144,12348,12350],{"className":8679,"code":12349,"language":8681,"meta":150,"style":150},"WHERE name LIKE 'zhizhi%'\n",[22,12351,12352],{"__ignoreMap":150},[203,12353,12354,12356,12358,12361],{"class":205,"line":206},[203,12355,8740],{"class":209},[203,12357,10901],{"class":209},[203,12359,12360],{"class":209}," LIKE",[203,12362,12363],{"class":223}," 'zhizhi%'\n",[18,12365,12366,12367,12370],{},"MySQL 可以定位到以 ",[22,12368,12369],{},"zhizhi"," 开头的索引范围。",[18,12372,12373],{},"以下写法无法确定字符串起始位置：",[144,12375,12377],{"className":8679,"code":12376,"language":8681,"meta":150,"style":150},"WHERE name LIKE '%zhizhi'\nWHERE name LIKE '%zhizhi%'\n",[22,12378,12379,12390],{"__ignoreMap":150},[203,12380,12381,12383,12385,12387],{"class":205,"line":206},[203,12382,8740],{"class":209},[203,12384,10901],{"class":209},[203,12386,12360],{"class":209},[203,12388,12389],{"class":223}," '%zhizhi'\n",[203,12391,12392,12394,12396,12398],{"class":205,"line":251},[203,12393,8740],{"class":209},[203,12395,10901],{"class":209},[203,12397,12360],{"class":209},[203,12399,12400],{"class":223}," '%zhizhi%'\n",[18,12402,12403],{},"普通 B+ 树索引通常无法据此快速定位范围。",[18,12405,12406],{},"如果查询是高频的任意位置文本搜索，应根据业务考虑：",[159,12408,12409,12412,12415],{},[162,12410,12411],{},"FULLTEXT 全文索引；",[162,12413,12414],{},"Elasticsearch 等搜索系统；",[162,12416,12417],{},"调整业务模型，保存可索引的前缀或结构化字段。",[18,12419,12420,12421,12423,12424,12427],{},"需要注意，如果查询需要的列全部包含在索引中，优化器仍可能选择扫描整个索引，因为索引比数据行更窄。此时执行计划中的 ",[22,12422,11318],{}," 可能是 ",[22,12425,12426],{},"index","，但这仍然是全索引扫描，并不是高效的范围查找。",[14,12429,12431],{"id":12430},"七or-中存在无法使用索引的分支","七、OR 中存在无法使用索引的分支",[18,12433,12434,12435,12438],{},"假设只有 ",[22,12436,12437],{},"user_id"," 有索引：",[144,12440,12442],{"className":8679,"code":12441,"language":8681,"meta":150,"style":150},"SELECT *\nFROM orders\nWHERE user_id = 100\n   OR remark = 'manual';\n",[22,12443,12444,12450,12456,12466],{"__ignoreMap":150},[203,12445,12446,12448],{"class":205,"line":206},[203,12447,8365],{"class":209},[203,12449,10532],{"class":209},[203,12451,12452,12454],{"class":205,"line":251},[203,12453,10537],{"class":209},[203,12455,8862],{"class":219},[203,12457,12458,12460,12462,12464],{"class":205,"line":260},[203,12459,8740],{"class":209},[203,12461,9499],{"class":219},[203,12463,495],{"class":209},[203,12465,9142],{"class":718},[203,12467,12468,12471,12474,12476,12479],{"class":205,"line":278},[203,12469,12470],{"class":209},"   OR",[203,12472,12473],{"class":219}," remark ",[203,12475,495],{"class":209},[203,12477,12478],{"class":223}," 'manual'",[203,12480,1297],{"class":219},[18,12482,12483,12484,12487],{},"即使第一部分能够使用索引，为了找出满足 ",[22,12485,12486],{},"remark='manual'"," 的记录，MySQL 仍可能需要扫描大量数据，最终选择全表扫描。",[18,12489,12490],{},"常见优化方式包括：",[2695,12492,12493,12496,12502,12508],{},[162,12494,12495],{},"为确实需要的条件建立合适索引；",[162,12497,12498,12499,2365],{},"在语义允许且执行计划更优时拆成两个查询并使用 ",[22,12500,12501],{},"UNION ALL",[162,12503,12504,12505,12507],{},"检查重复数据处理，不能为了用索引直接把 ",[22,12506,11279],{}," 改坏；",[162,12509,12510],{},"观察优化器是否采用 Index Merge，而不是假设多个单列索引一定会被组合。",[18,12512,4262,12513,12515],{},[22,12514,11279],{}," 两侧分别有可用索引，MySQL 可能选择 Index Merge，也可能根据成本选择其他计划。",[473,12517,12519],{"id":12518},"index-merge-是什么","Index Merge 是什么",[18,12521,12522,12523,12526],{},"Index Merge 是 MySQL 在",[110,12524,12525],{},"同一张表","上分别扫描多个索引，再合并这些索引扫描结果的访问方式。",[18,12528,12529],{},"假设存在两个单列索引：",[144,12531,12533],{"className":8679,"code":12532,"language":8681,"meta":150,"style":150},"KEY idx_user_id (user_id),\nKEY idx_status (status)\n",[22,12534,12535,12542],{"__ignoreMap":150},[203,12536,12537,12539],{"class":205,"line":206},[203,12538,11366],{"class":209},[203,12540,12541],{"class":219}," idx_user_id (user_id),\n",[203,12543,12544,12546,12549,12552],{"class":205,"line":251},[203,12545,11366],{"class":209},[203,12547,12548],{"class":219}," idx_status (",[203,12550,12551],{"class":209},"status",[203,12553,12343],{"class":219},[18,12555,11430],{},[144,12557,12559],{"className":8679,"code":12558,"language":8681,"meta":150,"style":150},"SELECT *\nFROM orders\nWHERE user_id = 100\n   OR status = 1;\n",[22,12560,12561,12567,12573,12583],{"__ignoreMap":150},[203,12562,12563,12565],{"class":205,"line":206},[203,12564,8365],{"class":209},[203,12566,10532],{"class":209},[203,12568,12569,12571],{"class":205,"line":251},[203,12570,10537],{"class":209},[203,12572,8862],{"class":219},[203,12574,12575,12577,12579,12581],{"class":205,"line":260},[203,12576,8740],{"class":209},[203,12578,9499],{"class":219},[203,12580,495],{"class":209},[203,12582,9142],{"class":718},[203,12584,12585,12587,12589,12591,12593],{"class":205,"line":278},[203,12586,12470],{"class":209},[203,12588,9511],{"class":209},[203,12590,9514],{"class":209},[203,12592,11601],{"class":718},[203,12594,1297],{"class":219},[18,12596,12597],{},"MySQL 可以分别得到：",[144,12599,12602],{"className":12600,"code":12601,"language":149,"meta":150},[147],"idx_user_id 扫描结果：满足 user_id=100 的行 ID\nidx_status  扫描结果：满足 status=1 的行 ID\n                         ↓\n                    合并并去重\n                         ↓\n                    读取完整记录\n",[22,12603,12601],{"__ignoreMap":150},[18,12605,12606,12607,12609],{},"在 ",[22,12608,11307],{}," 中通常表现为：",[144,12611,12614],{"className":12612,"code":12613,"language":149,"meta":150},[147],"type: index_merge\nkey: idx_user_id,idx_status\nExtra: Using union(...)\n",[22,12615,12613],{"__ignoreMap":150},[18,12617,12618],{},"Index Merge 主要有三种合并方式：",[299,12620,12621,12634],{},[302,12622,12623],{},[305,12624,12625,12628,12631],{},[308,12626,12627],{},"方式",[308,12629,12630],{},"适用逻辑",[308,12632,12633],{},"处理方式",[315,12635,12636,12653,12667],{},[305,12637,12638,12643,12650],{},[320,12639,12640],{},[22,12641,12642],{},"Using intersect(...)",[320,12644,12645,12646,12649],{},"多个条件用 ",[22,12647,12648],{},"AND"," 连接",[320,12651,12652],{},"取多个索引结果的交集",[305,12654,12655,12660,12664],{},[320,12656,12657],{},[22,12658,12659],{},"Using union(...)",[320,12661,12645,12662,12649],{},[22,12663,11279],{},[320,12665,12666],{},"取多个索引结果的并集",[305,12668,12669,12674,12680],{},[320,12670,12671],{},[22,12672,12673],{},"Using sort_union(...)",[320,12675,12676,12677,12679],{},"多个范围条件用 ",[22,12678,11279],{}," 连接，但不能直接使用 union 算法",[320,12681,12682],{},"收集全部行 ID，排序、去重后再读取记录",[473,12684,12686,12688],{"id":12685},"using-sort_union-是什么意思",[22,12687,12673],{}," 是什么意思",[18,12690,12691,12692,40,12695,12698],{},"先看一个典型查询。假设 ",[22,12693,12694],{},"key_col1",[22,12696,12697],{},"key_col2"," 分别建立了单列索引：",[144,12700,12702],{"className":8679,"code":12701,"language":8681,"meta":150,"style":150},"SELECT *\nFROM orders\nWHERE key_col1 \u003C 10\n   OR key_col2 \u003C 20;\n",[22,12703,12704,12710,12716,12727],{"__ignoreMap":150},[203,12705,12706,12708],{"class":205,"line":206},[203,12707,8365],{"class":209},[203,12709,10532],{"class":209},[203,12711,12712,12714],{"class":205,"line":251},[203,12713,10537],{"class":209},[203,12715,8862],{"class":219},[203,12717,12718,12720,12723,12725],{"class":205,"line":260},[203,12719,8740],{"class":209},[203,12721,12722],{"class":219}," key_col1 ",[203,12724,3306],{"class":209},[203,12726,11472],{"class":718},[203,12728,12729,12731,12734,12736,12739],{"class":205,"line":278},[203,12730,12470],{"class":209},[203,12732,12733],{"class":219}," key_col2 ",[203,12735,3306],{"class":209},[203,12737,12738],{"class":718}," 20",[203,12740,1297],{"class":219},[18,12742,12743],{},"优化器可以分别扫描两个索引，但这两个范围扫描产生的行 ID 不能直接按 union 算法边扫描边归并。此时可能采用 sort-union：",[144,12745,12748],{"className":12746,"code":12747,"language":149,"meta":150},[147],"扫描 key_col1 \u003C 10，收集行 ID\n                  \\\n                   → 汇总全部行 ID → 按行 ID 排序 → 去重 → 读取完整记录\n                  \u002F\n扫描 key_col2 \u003C 20，收集行 ID\n",[22,12749,12747],{"__ignoreMap":150},[18,12751,12752,12753,33],{},"以 InnoDB 二级索引为例，这里的“行 ID”通常可以理解为索引记录中携带的主键值。排序的对象是这些用于定位记录的标识，不是对最终查询结果执行 ",[22,12754,10615],{},[18,12756,12757,6440,12759,12761],{},[22,12758,12659],{},[22,12760,12673],{}," 的关键区别是：",[159,12763,12764,12770],{},[162,12765,12766,12769],{},[22,12767,12768],{},"union"," 可以对多个索引扫描产生的有序行 ID 流直接归并；",[162,12771,12772,12775],{},[22,12773,12774],{},"sort_union"," 需要先收集所有行 ID，再排序和去重，因此在完成收集与排序前不能开始返回记录。",[18,12777,12778,12779,12781,12782,12784],{},"所以 ",[22,12780,12774],{}," 通常比直接的 ",[22,12783,12768],{}," 多出行 ID 缓冲、排序和去重成本。候选记录很多时，优化器也可能认为全表扫描更便宜，最终不选择 Index Merge。",[18,12786,12787],{},"需要特别区分：",[159,12789,12790,12795,12802],{},[162,12791,12792,12794],{},[22,12793,12673],{}," 是同一张表内部多个索引扫描结果的合并算法；",[162,12796,12797,12798,12801],{},"SQL 的 ",[22,12799,12800],{},"UNION"," 是多个查询结果集的集合运算；",[162,12803,12804,12807,12808,12810],{},[22,12805,12806],{},"Using filesort"," 通常表示为了满足 ",[22,12809,10615],{}," 等要求进行额外排序。",[18,12812,12813],{},"这三者不是一回事。",[473,12815,12817],{"id":12816},"index-merge-从哪个版本开始支持","Index Merge 从哪个版本开始支持",[18,12819,12820,12821,12824],{},"Index Merge 从 ",[110,12822,12823],{},"MySQL 5.0"," 开始引入。在此之前，一张表在一次查询访问中通常至多选择一个索引；MySQL 5.0 开始可以在同一张表上扫描多个索引并合并结果。",[18,12826,12827,12828,53,12831,12834],{},"需要注意，Index Merge 本身是 MySQL 5.0 就有的访问方式，而用于显式影响它的 ",[22,12829,12830],{},"INDEX_MERGE",[22,12832,12833],{},"NO_INDEX_MERGE"," 优化器 Hint 是后续版本才增加的能力，不应把两者的版本混为一谈。",[18,12836,686],{},[144,12838,12840],{"className":8679,"code":12839,"language":8681,"meta":150,"style":150},"WHERE user_id = 100\n  AND status = 1\n",[22,12841,12842,12852],{"__ignoreMap":150},[203,12843,12844,12846,12848,12850],{"class":205,"line":206},[203,12845,8740],{"class":209},[203,12847,9499],{"class":219},[203,12849,495],{"class":209},[203,12851,9142],{"class":718},[203,12853,12854,12856,12858,12860],{"class":205,"line":251},[203,12855,9508],{"class":209},[203,12857,9511],{"class":209},[203,12859,9514],{"class":209},[203,12861,11460],{"class":718},[18,12863,12864],{},"优化器可能分别扫描两个索引后取交集。不过，很多情况下建立符合查询模式的联合索引：",[144,12866,12868],{"className":8679,"code":12867,"language":8681,"meta":150,"style":150},"KEY idx_user_status (user_id, status)\n",[22,12869,12870],{"__ignoreMap":150},[203,12871,12872,12874,12877,12879],{"class":205,"line":206},[203,12873,11366],{"class":209},[203,12875,12876],{"class":219}," idx_user_status (user_id, ",[203,12878,12551],{"class":209},[203,12880,12343],{"class":219},[18,12882,12883],{},"会比扫描并合并两个单列索引更直接。",[18,12885,12886],{},"因此 Index Merge 是优化器可选择的一种方案，不代表“多个单列索引一定等价于联合索引”。它也只合并同一张表上的索引扫描，不负责合并不同表的索引。",[14,12888,12890],{"id":12889},"八否定条件和低选择性条件","八、否定条件和低选择性条件",[18,12892,12893],{},"以下条件经常返回大量记录：",[144,12895,12897],{"className":8679,"code":12896,"language":8681,"meta":150,"style":150},"WHERE status != 1\nWHERE type NOT IN (1, 2)\nWHERE deleted_at IS NOT NULL\n",[22,12898,12899,12910,12934],{"__ignoreMap":150},[203,12900,12901,12903,12905,12908],{"class":205,"line":206},[203,12902,8740],{"class":209},[203,12904,9511],{"class":209},[203,12906,12907],{"class":209}," !=",[203,12909,11460],{"class":718},[203,12911,12912,12914,12917,12920,12923,12925,12927,12929,12932],{"class":205,"line":251},[203,12913,8740],{"class":209},[203,12915,12916],{"class":209}," type",[203,12918,12919],{"class":209}," NOT",[203,12921,12922],{"class":209}," IN",[203,12924,5163],{"class":219},[203,12926,2431],{"class":718},[203,12928,5179],{"class":219},[203,12930,12931],{"class":718},"2",[203,12933,12343],{"class":219},[203,12935,12936,12938,12941],{"class":205,"line":260},[203,12937,8740],{"class":209},[203,12939,12940],{"class":219}," deleted_at ",[203,12942,12943],{"class":209},"IS NOT NULL\n",[18,12945,12946],{},"这些操作符并不意味着语法上绝对不能使用索引。真正的问题通常是选择性：",[105,12948,12949],{},[18,12950,12951],{},"如果条件会命中表中大部分记录，使用二级索引定位后再逐条回表，可能比全表扫描更贵。",[18,12953,12954],{},"同理，以下字段即使有索引，也可能因为区分度太低而不被选择：",[144,12956,12959],{"className":12957,"code":12958,"language":149,"meta":150},[147],"性别\n是否删除\n只有少量枚举值的状态\n大量相同值的业务类型\n",[22,12960,12958],{"__ignoreMap":150},[18,12962,12963],{},"低基数字段并非永远不适合索引。如果它与其他字段组成联合索引、查询命中比例很低，或者能够形成覆盖索引，依然可能产生价值。",[18,12965,11816,12966,12969],{},[22,12967,12968],{},"IS NULL"," 可以使用索引，不应把它列为固定的索引失效规则。",[14,12971,12973],{"id":12972},"九查询返回数据过多","九、查询返回数据过多",[18,12975,12976],{},"假设表中有 100 万行，而条件预计返回 80 万行：",[144,12978,12980],{"className":8679,"code":12979,"language":8681,"meta":150,"style":150},"SELECT *\nFROM orders\nWHERE status = 1;\n",[22,12981,12982,12988,12994],{"__ignoreMap":150},[203,12983,12984,12986],{"class":205,"line":206},[203,12985,8365],{"class":209},[203,12987,10532],{"class":209},[203,12989,12990,12992],{"class":205,"line":251},[203,12991,10537],{"class":209},[203,12993,8862],{"class":219},[203,12995,12996,12998,13000,13002,13004],{"class":205,"line":260},[203,12997,8740],{"class":209},[203,12999,9511],{"class":209},[203,13001,9514],{"class":209},[203,13003,11601],{"class":718},[203,13005,1297],{"class":219},[18,13007,4262,13008,13010],{},[22,13009,12551],{}," 是二级索引，执行过程可能是：",[144,13012,13015],{"className":13013,"code":13014,"language":149,"meta":150},[147],"扫描大量二级索引记录\n        ↓\n根据主键逐条回表\n        ↓\n读取完整行\n",[22,13016,13014],{"__ignoreMap":150},[18,13018,13019],{},"大量离散回表的成本可能高于直接扫描聚簇索引，因此优化器会主动选择全表扫描。",[18,13021,13022],{},"可以从以下方向优化：",[159,13024,13025,13028,13031,13034,13037,13040],{},[162,13026,13027],{},"确认业务是否真的需要返回如此多的数据；",[162,13029,13030],{},"增加更有区分度的查询条件；",[162,13032,13033],{},"建立符合查询模式的联合索引；",[162,13035,13036],{},"只查询必要字段，评估覆盖索引；",[162,13038,13039],{},"分页或分批处理；",[162,13041,13042,13043,13046],{},"避免用 ",[22,13044,13045],{},"FORCE INDEX"," 掩盖不合理的数据访问方式。",[14,13048,13050],{"id":13049},"十order-by-或-group-by-没有匹配索引顺序","十、ORDER BY 或 GROUP BY 没有匹配索引顺序",[18,13052,13053,13054,13056],{},"索引不仅可以用于 ",[22,13055,8740],{}," 过滤，也可以帮助排序和分组，但需要满足相应的顺序条件。",[18,13058,13059],{},"例如索引：",[144,13061,13062],{"className":8679,"code":11359,"language":8681,"meta":150,"style":150},[22,13063,13064],{"__ignoreMap":150},[203,13065,13066,13068],{"class":205,"line":206},[203,13067,11366],{"class":209},[203,13069,11369],{"class":219},[18,13071,11430],{},[144,13073,13075],{"className":8679,"code":13074,"language":8681,"meta":150,"style":150},"SELECT *\nFROM orders\nWHERE user_id = 100\nORDER BY created_at;\n",[22,13076,13077,13083,13089,13099],{"__ignoreMap":150},[203,13078,13079,13081],{"class":205,"line":206},[203,13080,8365],{"class":209},[203,13082,10532],{"class":209},[203,13084,13085,13087],{"class":205,"line":251},[203,13086,10537],{"class":209},[203,13088,8862],{"class":219},[203,13090,13091,13093,13095,13097],{"class":205,"line":260},[203,13092,8740],{"class":209},[203,13094,9499],{"class":219},[203,13096,495],{"class":209},[203,13098,9142],{"class":718},[203,13100,13101,13103],{"class":205,"line":278},[203,13102,10615],{"class":209},[203,13104,13105],{"class":219}," created_at;\n",[18,13107,13108,13109,13111,13112,13114],{},"通常可以利用索引中同一 ",[22,13110,12437],{}," 下 ",[22,13113,11832],{}," 有序的特点。",[18,13116,13117],{},"但下面的查询未限定最左列：",[144,13119,13121],{"className":8679,"code":13120,"language":8681,"meta":150,"style":150},"SELECT *\nFROM orders\nORDER BY created_at;\n",[22,13122,13123,13129,13135],{"__ignoreMap":150},[203,13124,13125,13127],{"class":205,"line":206},[203,13126,8365],{"class":209},[203,13128,10532],{"class":209},[203,13130,13131,13133],{"class":205,"line":251},[203,13132,10537],{"class":209},[203,13134,8862],{"class":219},[203,13136,13137,13139],{"class":205,"line":260},[203,13138,10615],{"class":209},[203,13140,13105],{"class":219},[18,13142,13143,13145],{},[22,13144,11832],{}," 在整棵联合索引中并不是全局连续有序，通常无法直接利用该索引消除排序。",[18,13147,13148],{},"以下情况也可能无法直接使用索引顺序：",[159,13150,13151,13154,13157,13160,13163],{},[162,13152,13153],{},"排序列顺序与联合索引不匹配；",[162,13155,13156],{},"联合索引中，排序列前面存在没有被固定为常量的索引列；",[162,13158,13159],{},"对排序列执行表达式或函数；",[162,13161,13162],{},"多张表连接后按非驱动表字段排序；",[162,13164,13165],{},"混合升降序与索引定义不匹配。",[473,13167,13168],{"id":13168},"过滤条件如何影响索引顺序",[18,13170,11536],{},[144,13172,13173],{"className":8679,"code":11418,"language":8681,"meta":150,"style":150},[22,13174,13175],{"__ignoreMap":150},[203,13176,13177,13179],{"class":205,"line":206},[203,13178,11366],{"class":209},[203,13180,11427],{"class":219},[18,13182,13183],{},"下面的查询可以利用索引顺序：",[144,13185,13187],{"className":8679,"code":13186,"language":8681,"meta":150,"style":150},"SELECT *\nFROM t\nWHERE a = 1\n  AND b = 2\nORDER BY c;\n",[22,13188,13189,13195,13201,13211,13221],{"__ignoreMap":150},[203,13190,13191,13193],{"class":205,"line":206},[203,13192,8365],{"class":209},[203,13194,10532],{"class":209},[203,13196,13197,13199],{"class":205,"line":251},[203,13198,10537],{"class":209},[203,13200,11448],{"class":219},[203,13202,13203,13205,13207,13209],{"class":205,"line":260},[203,13204,8740],{"class":209},[203,13206,11455],{"class":219},[203,13208,495],{"class":209},[203,13210,11460],{"class":718},[203,13212,13213,13215,13217,13219],{"class":205,"line":278},[203,13214,9508],{"class":209},[203,13216,11467],{"class":219},[203,13218,495],{"class":209},[203,13220,11610],{"class":718},[203,13222,13223,13225],{"class":205,"line":285},[203,13224,10615],{"class":209},[203,13226,13227],{"class":219}," c;\n",[18,13229,13230,13231,13233,13234,13236],{},"因为 ",[22,13232,11243],{}," 都固定为常量，剩余记录天然按照 ",[22,13235,11247],{}," 排序：",[144,13238,13241],{"className":13239,"code":13240,"language":149,"meta":150},[147],"(a=1, b=2, c=1)\n(a=1, b=2, c=2)\n(a=1, b=2, c=3)\n",[22,13242,13240],{"__ignoreMap":150},[18,13244,13245,13246,431],{},"如果只固定 ",[22,13247,119],{},[144,13249,13251],{"className":8679,"code":13250,"language":8681,"meta":150,"style":150},"SELECT *\nFROM t\nWHERE a = 1\nORDER BY c;\n",[22,13252,13253,13259,13265,13275],{"__ignoreMap":150},[203,13254,13255,13257],{"class":205,"line":206},[203,13256,8365],{"class":209},[203,13258,10532],{"class":209},[203,13260,13261,13263],{"class":205,"line":251},[203,13262,10537],{"class":209},[203,13264,11448],{"class":219},[203,13266,13267,13269,13271,13273],{"class":205,"line":260},[203,13268,8740],{"class":209},[203,13270,11455],{"class":219},[203,13272,495],{"class":209},[203,13274,11460],{"class":718},[203,13276,13277,13279],{"class":205,"line":278},[203,13278,10615],{"class":209},[203,13280,13227],{"class":219},[18,13282,13283,13284,13286,13287,13289,13290,13236],{},"索引记录先按照 ",[22,13285,11496],{}," 排序，再在相同 ",[22,13288,11496],{}," 中按照 ",[22,13291,11247],{},[144,13293,13296],{"className":13294,"code":13295,"language":149,"meta":150},[147],"(a=1, b=1, c=1)\n(a=1, b=1, c=9)\n(a=1, b=2, c=2)\n(a=1, b=2, c=8)\n",[22,13297,13295],{"__ignoreMap":150},[18,13299,13300,13301,13303,13304,13307,13308,33],{},"从整体看，",[22,13302,11247],{}," 的顺序是 ",[22,13305,13306],{},"1、9、2、8","，并不是全局有序，因此通常仍需 ",[22,13309,13310],{},"filesort",[18,13312,13313],{},"范围条件也可能产生类似效果：",[144,13315,13317],{"className":8679,"code":13316,"language":8681,"meta":150,"style":150},"SELECT *\nFROM t\nWHERE a = 1\n  AND b > 10\nORDER BY c;\n",[22,13318,13319,13325,13331,13341,13351],{"__ignoreMap":150},[203,13320,13321,13323],{"class":205,"line":206},[203,13322,8365],{"class":209},[203,13324,10532],{"class":209},[203,13326,13327,13329],{"class":205,"line":251},[203,13328,10537],{"class":209},[203,13330,11448],{"class":219},[203,13332,13333,13335,13337,13339],{"class":205,"line":260},[203,13334,8740],{"class":209},[203,13336,11455],{"class":219},[203,13338,495],{"class":209},[203,13340,11460],{"class":718},[203,13342,13343,13345,13347,13349],{"class":205,"line":278},[203,13344,9508],{"class":209},[203,13346,11467],{"class":219},[203,13348,9371],{"class":209},[203,13350,11472],{"class":718},[203,13352,13353,13355],{"class":205,"line":285},[203,13354,10615],{"class":209},[203,13356,13227],{"class":219},[18,13358,13359,13360,13362,13363,13365,13366,13368,13369,13371],{},"扫描结果首先按不同的 ",[22,13361,11496],{}," 值分组，每个 ",[22,13364,11496],{}," 内部才按 ",[22,13367,11247],{}," 排序，所以不能直接保证全局 ",[22,13370,11247],{}," 有序。",[473,13373,13374],{"id":13374},"为什么按非驱动表字段排序通常需要额外排序",[18,13376,13377],{},"假设查询：",[144,13379,13381],{"className":8679,"code":13380,"language":8681,"meta":150,"style":150},"SELECT o.id, u.name\nFROM orders o\nJOIN users u ON u.id = o.user_id\nWHERE o.created_at >= '2026-07-01'\nORDER BY u.name;\n",[22,13382,13383,13404,13411,13437,13453],{"__ignoreMap":150},[203,13384,13385,13387,13390,13392,13394,13396,13399,13401],{"class":205,"line":206},[203,13386,8365],{"class":209},[203,13388,13389],{"class":718}," o",[203,13391,9766],{"class":219},[203,13393,9155],{"class":718},[203,13395,5179],{"class":219},[203,13397,13398],{"class":718},"u",[203,13400,9766],{"class":219},[203,13402,13403],{"class":718},"name\n",[203,13405,13406,13408],{"class":205,"line":251},[203,13407,10537],{"class":209},[203,13409,13410],{"class":219}," orders o\n",[203,13412,13413,13416,13419,13421,13424,13426,13428,13430,13432,13434],{"class":205,"line":260},[203,13414,13415],{"class":209},"JOIN",[203,13417,13418],{"class":219}," users u ",[203,13420,9565],{"class":209},[203,13422,13423],{"class":718}," u",[203,13425,9766],{"class":219},[203,13427,9155],{"class":718},[203,13429,9514],{"class":209},[203,13431,13389],{"class":718},[203,13433,9766],{"class":219},[203,13435,13436],{"class":718},"user_id\n",[203,13438,13439,13441,13443,13445,13447,13450],{"class":205,"line":278},[203,13440,8740],{"class":209},[203,13442,13389],{"class":718},[203,13444,9766],{"class":219},[203,13446,11832],{"class":718},[203,13448,13449],{"class":209}," >=",[203,13451,13452],{"class":223}," '2026-07-01'\n",[203,13454,13455,13457,13459,13461,13463],{"class":205,"line":285},[203,13456,10615],{"class":209},[203,13458,13423],{"class":718},[203,13460,9766],{"class":219},[203,13462,10870],{"class":718},[203,13464,1297],{"class":219},[18,13466,13467,13468,13471,13472,13475,13476,13478],{},"假设优化器选择 ",[22,13469,13470],{},"orders"," 作为第一个非 ",[22,13473,13474],{},"const"," 表，也就是先按订单时间读取订单，再逐条根据 ",[22,13477,12437],{}," 查询用户：",[144,13480,13483],{"className":13481,"code":13482,"language":149,"meta":150},[147],"按 orders.created_at 读取订单\n            ↓\n逐条查找对应 users\n            ↓\n得到的 u.name 没有全局顺序\n            ↓\n对连接结果执行 filesort\n",[22,13484,13482],{"__ignoreMap":150},[18,13486,13487,13488,13491,13492,13495],{},"即使 ",[22,13489,13490],{},"users.name"," 上有索引，这个索引也只保证从 ",[22,13493,13494],{},"users"," 出发扫描时的姓名顺序，不能保证“先读取订单、再查用户”得到的连接结果按姓名有序。",[18,13497,13498,13499,13501,13502,13504,13505,13508],{},"如果优化器能够把 ",[22,13500,13494],{}," 调整为驱动表，并且从 ",[22,13503,13490],{}," 索引按顺序读取，再连接订单，就可能利用该顺序。但是否能够这样执行，还取决于过滤条件、连接关系、成本估算和 ",[22,13506,13507],{},"LIMIT"," 等因素。",[473,13510,13511],{"id":13511},"索引方向与查询方向如何匹配",[18,13513,13514],{},"MySQL 8.0 支持降序索引，也支持反向扫描 B+ 树索引。",[18,13516,13517],{},"索引：",[144,13519,13521],{"className":8679,"code":13520,"language":8681,"meta":150,"style":150},"KEY idx_ab (a ASC, b ASC)\n",[22,13522,13523],{"__ignoreMap":150},[203,13524,13525,13527,13530,13533,13536,13538],{"class":205,"line":206},[203,13526,11366],{"class":209},[203,13528,13529],{"class":219}," idx_ab (a ",[203,13531,13532],{"class":209},"ASC",[203,13534,13535],{"class":219},", b ",[203,13537,13532],{"class":209},[203,13539,12343],{"class":219},[18,13541,13542],{},"通常可以支持：",[144,13544,13546],{"className":8679,"code":13545,"language":8681,"meta":150,"style":150},"ORDER BY a ASC,  b ASC\nORDER BY a DESC, b DESC\n",[22,13547,13548,13562],{"__ignoreMap":150},[203,13549,13550,13552,13554,13556,13559],{"class":205,"line":206},[203,13551,10615],{"class":209},[203,13553,11455],{"class":219},[203,13555,13532],{"class":209},[203,13557,13558],{"class":219},",  b ",[203,13560,13561],{"class":209},"ASC\n",[203,13563,13564,13566,13568,13571,13573],{"class":205,"line":251},[203,13565,10615],{"class":209},[203,13567,11455],{"class":219},[203,13569,13570],{"class":209},"DESC",[203,13572,13535],{"class":219},[203,13574,13575],{"class":209},"DESC\n",[18,13577,13578],{},"第二条可以通过整体反向扫描完成。",[18,13580,13581],{},"但混合方向：",[144,13583,13585],{"className":8679,"code":13584,"language":8681,"meta":150,"style":150},"ORDER BY a ASC, b DESC\n",[22,13586,13587],{"__ignoreMap":150},[203,13588,13589,13591,13593,13595,13597],{"class":205,"line":206},[203,13590,10615],{"class":209},[203,13592,11455],{"class":219},[203,13594,13532],{"class":209},[203,13596,13535],{"class":219},[203,13598,13575],{"class":209},[18,13600,13601,13602,13605],{},"与 ",[22,13603,13604],{},"(a ASC, b ASC)"," 的排序关系不同。要直接使用索引顺序，应建立方向相匹配的索引：",[144,13607,13609],{"className":8679,"code":13608,"language":8681,"meta":150,"style":150},"KEY idx_ab_mixed (a ASC, b DESC)\n",[22,13610,13611],{"__ignoreMap":150},[203,13612,13613,13615,13618,13620,13622,13624],{"class":205,"line":206},[203,13614,11366],{"class":209},[203,13616,13617],{"class":219}," idx_ab_mixed (a ",[203,13619,13532],{"class":209},[203,13621,13535],{"class":219},[203,13623,13570],{"class":209},[203,13625,12343],{"class":219},[18,13627,13628],{},"这个索引也可以通过整体反向扫描支持：",[144,13630,13632],{"className":8679,"code":13631,"language":8681,"meta":150,"style":150},"ORDER BY a DESC, b ASC\n",[22,13633,13634],{"__ignoreMap":150},[203,13635,13636,13638,13640,13642,13644],{"class":205,"line":206},[203,13637,10615],{"class":209},[203,13639,11455],{"class":219},[203,13641,13570],{"class":209},[203,13643,13535],{"class":219},[203,13645,13561],{"class":209},[18,13647,13648],{},"所以更准确的规则是：",[105,13650,13651],{},[18,13652,13653],{},"所有列同向时，升序索引可以正向或反向扫描；升降序混合时，索引各列的方向关系需要与查询一致，或者与查询整体相反。",[18,13655,13656,13657,13659],{},"MySQL 5.7 及更早版本虽然允许在索引定义中书写 ",[22,13658,13570],{},"，但通常不会真正按降序存储键值。讨论索引方向时必须结合实际 MySQL 版本。",[18,13661,13662,13663,13665,13666,13669,13670,33],{},"此时 ",[22,13664,11307],{}," 的 ",[22,13667,13668],{},"Extra"," 中可能出现 ",[22,13671,12806],{},[18,13673,13674,13675,13677],{},"需要注意，",[22,13676,12806],{}," 表示没有直接利用索引顺序完成排序，并不保证真的使用磁盘文件；数据较小时也可能在内存中排序。",[14,13679,13681],{"id":13680},"十一索引统计信息不准确","十一、索引统计信息不准确",[18,13683,13684],{},"MySQL 优化器依赖统计信息估算：",[159,13686,13687,13690,13693,13696],{},[162,13688,13689],{},"条件会命中多少行；",[162,13691,13692],{},"不同索引的选择性；",[162,13694,13695],{},"扫描和回表成本；",[162,13697,13698],{},"JOIN 的顺序及成本。",[18,13700,13701],{},"如果数据分布发生剧烈变化，而统计信息未能反映真实情况，优化器可能选错索引或改用全表扫描。",[18,13703,13704],{},"可以先通过执行计划验证估算行数与实际行数是否偏差明显，再根据情况评估：",[144,13706,13708],{"className":8679,"code":13707,"language":8681,"meta":150,"style":150},"ANALYZE TABLE orders;\n",[22,13709,13710],{"__ignoreMap":150},[203,13711,13712,13715,13718],{"class":205,"line":206},[203,13713,13714],{"class":219},"ANALYZE ",[203,13716,13717],{"class":209},"TABLE",[203,13719,13720],{"class":219}," orders;\n",[18,13722,13723],{},"MySQL 8.0 还可以使用直方图帮助优化器理解非索引列或数据分布不均匀字段的值分布。",[18,13725,13726],{},"不要在没有验证的情况下频繁更新统计信息，也不要把所有执行计划问题都归因于统计信息。",[14,13728,13730],{"id":13729},"十二如何用-explain-判断","十二、如何用 EXPLAIN 判断",[18,13732,13733],{},"基础排查可以执行：",[144,13735,13737],{"className":8679,"code":13736,"language":8681,"meta":150,"style":150},"EXPLAIN\nSELECT *\nFROM orders\nWHERE user_id = 100;\n",[22,13738,13739,13744,13750,13756],{"__ignoreMap":150},[203,13740,13741],{"class":205,"line":206},[203,13742,13743],{"class":219},"EXPLAIN\n",[203,13745,13746,13748],{"class":205,"line":251},[203,13747,8365],{"class":209},[203,13749,10532],{"class":209},[203,13751,13752,13754],{"class":205,"line":260},[203,13753,10537],{"class":209},[203,13755,8862],{"class":219},[203,13757,13758,13760,13762,13764,13766],{"class":205,"line":278},[203,13759,8740],{"class":209},[203,13761,9499],{"class":219},[203,13763,495],{"class":209},[203,13765,8749],{"class":718},[203,13767,1297],{"class":219},[18,13769,13770],{},"重点观察：",[299,13772,13773,13782],{},[302,13774,13775],{},[305,13776,13777,13779],{},[308,13778,2550],{},[308,13780,13781],{},"关注点",[315,13783,13784,13793,13802,13812,13834,13844,13854],{},[305,13785,13786,13790],{},[320,13787,13788],{},[22,13789,11303],{},[320,13791,13792],{},"理论上可能使用哪些索引",[305,13794,13795,13799],{},[320,13796,13797],{},[22,13798,11314],{},[320,13800,13801],{},"优化器最终选择了哪个索引",[305,13803,13804,13809],{},[320,13805,13806],{},[22,13807,13808],{},"key_len",[320,13810,13811],{},"实际使用的索引长度，可辅助判断联合索引用到哪一部分",[305,13813,13814,13818],{},[320,13815,13816],{},[22,13817,11318],{},[320,13819,13820,13821,53,13823,53,13826,53,13829,53,13831],{},"访问方式，如 ",[22,13822,13474],{},[22,13824,13825],{},"ref",[22,13827,13828],{},"range",[22,13830,12426],{},[22,13832,13833],{},"ALL",[305,13835,13836,13841],{},[320,13837,13838],{},[22,13839,13840],{},"rows",[320,13842,13843],{},"预计需要检查的行数",[305,13845,13846,13851],{},[320,13847,13848],{},[22,13849,13850],{},"filtered",[320,13852,13853],{},"经过表条件过滤后预计保留的百分比",[305,13855,13856,13860],{},[320,13857,13858],{},[22,13859,13668],{},[320,13861,13862],{},"是否覆盖索引、ICP、额外排序或临时表等",[473,13864,13866,13867,13869],{"id":13865},"_1-type-到底表示什么","1. ",[22,13868,11318],{}," 到底表示什么",[18,13871,13872,13874,13875,13878,13879,33],{},[22,13873,11318],{}," 表示 MySQL 访问当前表中记录的方式。在 ",[22,13876,13877],{},"EXPLAIN FORMAT=JSON"," 中，对应字段名是 ",[22,13880,13881],{},"access_type",[18,13883,13884],{},"它回答的不是“有没有索引”这么简单，而是：",[105,13886,13887],{},[18,13888,13889],{},"MySQL 准备通过唯一键直接取一行、按普通索引查一组记录、扫描一个索引范围，还是扫描整棵索引或整张表？",[18,13891,13892],{},"官方文档通常按照下面的顺序描述常见访问类型：",[144,13894,13897],{"className":13895,"code":13896,"language":149,"meta":150},[147],"system\n  ↓\nconst\n  ↓\neq_ref\n  ↓\nref\n  ↓\nrange\n  ↓\nindex\n  ↓\nALL\n",[22,13898,13896],{"__ignoreMap":150},[18,13900,13901],{},"越靠前通常代表定位越精确，但这不是脱离数据量的绝对性能排名。",[18,13903,686],{},[159,13905,13906,13912,13920,13925],{},[162,13907,13908,13909,13911],{},"小表使用 ",[22,13910,13833],{}," 可能只扫描几十行，成本很低；",[162,13913,13914,13916,13917,13919],{},[22,13915,13825],{}," 虽然比 ",[22,13918,13828],{}," 靠前，但如果某个值匹配几十万行，仍然可能很慢；",[162,13921,13922,13924],{},[22,13923,13828],{}," 如果只扫描十行，通常会非常高效；",[162,13926,13927,13929],{},[22,13928,12426],{}," 虽然使用了索引，却可能把整个索引扫描一遍。",[18,13931,13932,13933,13935,13936,13939],{},"所以查看 ",[22,13934,11318],{}," 后，还必须结合 ",[22,13937,13938],{},"key、rows、filtered、Extra"," 以及实际耗时判断。",[473,13941,13943,13944,13947],{"id":13942},"_2-system表中只有一行","2. ",[22,13945,13946],{},"system","：表中只有一行",[18,13949,13950,2014,13952,13954],{},[22,13951,13946],{},[22,13953,13474],{}," 的特殊情况，表示优化器判断当前表只有一行。",[18,13956,13957],{},"访问过程可以理解为：",[144,13959,13962],{"className":13960,"code":13961,"language":149,"meta":150},[147],"整张表只有一行\n      ↓\n直接读取这一行\n",[22,13963,13961],{"__ignoreMap":150},[18,13965,13966,13967,13969],{},"它在普通 InnoDB 业务表的执行计划中并不常见。看到 ",[22,13968,13946],{}," 时，不需要把它理解成某种特殊索引；它表达的是表规模已经确定为单行，因此没有查找范围可言。",[473,13971,6437,13973,13975],{"id":13972},"_3-const通过主键或唯一键最多定位一行",[22,13974,13474],{},"：通过主键或唯一键最多定位一行",[18,13977,13978,13980],{},[22,13979,13474],{}," 表示当前表最多只有一条记录满足条件，并且这条记录在查询开始时只需要读取一次。",[18,13982,13983],{},"典型场景是使用常量匹配主键或完整唯一索引：",[144,13985,13987],{"className":8679,"code":13986,"language":8681,"meta":150,"style":150},"CREATE TABLE users (\n    id BIGINT PRIMARY KEY,\n    email VARCHAR(128) NOT NULL,\n    UNIQUE KEY uk_email (email)\n);\n",[22,13988,13989,14001,14014,14033,14044],{"__ignoreMap":150},[203,13990,13991,13993,13995,13998],{"class":205,"line":206},[203,13992,9045],{"class":209},[203,13994,12167],{"class":209},[203,13996,13997],{"class":215}," users",[203,13999,14000],{"class":219}," (\n",[203,14002,14003,14006,14009,14012],{"class":205,"line":251},[203,14004,14005],{"class":219},"    id ",[203,14007,14008],{"class":209},"BIGINT",[203,14010,14011],{"class":209}," PRIMARY KEY",[203,14013,1569],{"class":219},[203,14015,14016,14019,14021,14023,14026,14029,14031],{"class":205,"line":260},[203,14017,14018],{"class":219},"    email ",[203,14020,12235],{"class":209},[203,14022,220],{"class":219},[203,14024,14025],{"class":718},"128",[203,14027,14028],{"class":219},") ",[203,14030,10784],{"class":209},[203,14032,1569],{"class":219},[203,14034,14035,14038,14041],{"class":205,"line":278},[203,14036,14037],{"class":209},"    UNIQUE",[203,14039,14040],{"class":209}," KEY",[203,14042,14043],{"class":219}," uk_email (email)\n",[203,14045,14046],{"class":205,"line":285},[203,14047,227],{"class":219},[18,14049,14050],{},"查询主键：",[144,14052,14054],{"className":8679,"code":14053,"language":8681,"meta":150,"style":150},"SELECT *\nFROM users\nWHERE id = 100;\n",[22,14055,14056,14062,14068],{"__ignoreMap":150},[203,14057,14058,14060],{"class":205,"line":206},[203,14059,8365],{"class":209},[203,14061,10532],{"class":209},[203,14063,14064,14066],{"class":205,"line":251},[203,14065,10537],{"class":209},[203,14067,10894],{"class":219},[203,14069,14070,14072,14074,14076,14078],{"class":205,"line":260},[203,14071,8740],{"class":209},[203,14073,9137],{"class":219},[203,14075,495],{"class":209},[203,14077,8749],{"class":718},[203,14079,1297],{"class":219},[18,14081,14082],{},"或者查询唯一键：",[144,14084,14086],{"className":8679,"code":14085,"language":8681,"meta":150,"style":150},"SELECT *\nFROM users\nWHERE email = 'tom@example.com';\n",[22,14087,14088,14094,14100],{"__ignoreMap":150},[203,14089,14090,14092],{"class":205,"line":206},[203,14091,8365],{"class":209},[203,14093,10532],{"class":209},[203,14095,14096,14098],{"class":205,"line":251},[203,14097,10537],{"class":209},[203,14099,10894],{"class":219},[203,14101,14102,14104,14107,14109,14112],{"class":205,"line":260},[203,14103,8740],{"class":209},[203,14105,14106],{"class":219}," email ",[203,14108,495],{"class":209},[203,14110,14111],{"class":223}," 'tom@example.com'",[203,14113,1297],{"class":219},[18,14115,14116],{},"由于主键或唯一键能够保证最多返回一行，MySQL 可以直接定位目标记录：",[144,14118,14121],{"className":14119,"code":14120,"language":149,"meta":150},[147],"常量值\n  ↓\n主键或唯一索引查找\n  ↓\n最多一条记录\n",[22,14122,14120],{"__ignoreMap":150},[18,14124,14125,14126,431],{},"如果是联合唯一索引，需要把能够确定唯一记录的索引列完整匹配，才容易形成 ",[22,14127,13474],{},[144,14129,14131],{"className":8679,"code":14130,"language":8681,"meta":150,"style":150},"UNIQUE KEY uk_tenant_order (tenant_id, order_no)\n",[22,14132,14133],{"__ignoreMap":150},[203,14134,14135,14138,14140],{"class":205,"line":206},[203,14136,14137],{"class":209},"UNIQUE",[203,14139,14040],{"class":209},[203,14141,14142],{"class":219}," uk_tenant_order (tenant_id, order_no)\n",[144,14144,14146],{"className":8679,"code":14145,"language":8681,"meta":150,"style":150},"WHERE tenant_id = 10\n  AND order_no = 'A1001'\n",[22,14147,14148,14159],{"__ignoreMap":150},[203,14149,14150,14152,14155,14157],{"class":205,"line":206},[203,14151,8740],{"class":209},[203,14153,14154],{"class":219}," tenant_id ",[203,14156,495],{"class":209},[203,14158,11472],{"class":718},[203,14160,14161,14163,14165,14167],{"class":205,"line":251},[203,14162,9508],{"class":209},[203,14164,12265],{"class":219},[203,14166,495],{"class":209},[203,14168,14169],{"class":223}," 'A1001'\n",[18,14171,14172],{},"如果只查询：",[144,14174,14176],{"className":8679,"code":14175,"language":8681,"meta":150,"style":150},"WHERE tenant_id = 10\n",[22,14177,14178],{"__ignoreMap":150},[203,14179,14180,14182,14184,14186],{"class":205,"line":206},[203,14181,8740],{"class":209},[203,14183,14154],{"class":219},[203,14185,495],{"class":209},[203,14187,11472],{"class":718},[18,14189,14190,14191,33],{},"可能返回多行，通常就不是 ",[22,14192,13474],{},[18,14194,14195,14197],{},[22,14196,13474],{}," 的核心不是“使用了等号”，而是：",[105,14199,14200],{},[18,14201,14202],{},"通过常量和唯一约束，能够确定当前表最多只返回一条记录。",[473,14204,14206,14207,14210],{"id":14205},"_4-eq_refjoin-时每条前表记录最多匹配一行","4. ",[22,14208,14209],{},"eq_ref","：JOIN 时每条前表记录最多匹配一行",[18,14212,14213,14215],{},[22,14214,14209],{}," 最常见于多表 JOIN。",[18,14217,14218,14219,14222],{},"它表示对于前面表产生的每一条记录，MySQL 都通过主键或 ",[22,14220,14221],{},"UNIQUE NOT NULL"," 索引，在当前表中最多查到一条记录。",[18,14224,686],{},[144,14226,14228],{"className":8679,"code":14227,"language":8681,"meta":150,"style":150},"CREATE TABLE orders (\n    id BIGINT PRIMARY KEY,\n    user_id BIGINT NOT NULL,\n    KEY idx_user_id (user_id)\n);\n",[22,14229,14230,14241,14251,14263,14271],{"__ignoreMap":150},[203,14231,14232,14234,14236,14239],{"class":205,"line":206},[203,14233,9045],{"class":209},[203,14235,12167],{"class":209},[203,14237,14238],{"class":215}," orders",[203,14240,14000],{"class":219},[203,14242,14243,14245,14247,14249],{"class":205,"line":251},[203,14244,14005],{"class":219},[203,14246,14008],{"class":209},[203,14248,14011],{"class":209},[203,14250,1569],{"class":219},[203,14252,14253,14256,14258,14261],{"class":205,"line":260},[203,14254,14255],{"class":219},"    user_id ",[203,14257,14008],{"class":209},[203,14259,14260],{"class":209}," NOT NULL",[203,14262,1569],{"class":219},[203,14264,14265,14268],{"class":205,"line":278},[203,14266,14267],{"class":209},"    KEY",[203,14269,14270],{"class":219}," idx_user_id (user_id)\n",[203,14272,14273],{"class":205,"line":285},[203,14274,227],{"class":219},[144,14276,14278],{"className":8679,"code":14277,"language":8681,"meta":150,"style":150},"SELECT o.id, u.name\nFROM orders o\nJOIN users u ON u.id = o.user_id;\n",[22,14279,14280,14298,14304],{"__ignoreMap":150},[203,14281,14282,14284,14286,14288,14290,14292,14294,14296],{"class":205,"line":206},[203,14283,8365],{"class":209},[203,14285,13389],{"class":718},[203,14287,9766],{"class":219},[203,14289,9155],{"class":718},[203,14291,5179],{"class":219},[203,14293,13398],{"class":718},[203,14295,9766],{"class":219},[203,14297,13403],{"class":718},[203,14299,14300,14302],{"class":205,"line":251},[203,14301,10537],{"class":209},[203,14303,13410],{"class":219},[203,14305,14306,14308,14310,14312,14314,14316,14318,14320,14322,14324,14326],{"class":205,"line":260},[203,14307,13415],{"class":209},[203,14309,13418],{"class":219},[203,14311,9565],{"class":209},[203,14313,13423],{"class":718},[203,14315,9766],{"class":219},[203,14317,9155],{"class":718},[203,14319,9514],{"class":209},[203,14321,13389],{"class":718},[203,14323,9766],{"class":219},[203,14325,12437],{"class":718},[203,14327,1297],{"class":219},[18,14329,14330,14331,14333,14334,11236,14337,431],{},"假设执行顺序是先读取 ",[22,14332,13470],{},"，再根据 ",[22,14335,14336],{},"o.user_id",[22,14338,13494],{},[144,14340,14343],{"className":14341,"code":14342,"language":149,"meta":150},[147],"orders 的一条记录\n        ↓\nu.id = o.user_id\n        ↓\n通过 users 主键最多定位一行\n",[22,14344,14342],{"__ignoreMap":150},[18,14346,13662,14347,14349,14350,33],{},[22,14348,13494],{}," 表的访问类型可能是 ",[22,14351,14209],{},[18,14353,14354,40,14356,14358],{},[22,14355,13474],{},[22,14357,14209],{}," 的区别是：",[299,14360,14361,14373],{},[302,14362,14363],{},[305,14364,14365,14367,14370],{},[308,14366,310],{},[308,14368,14369],{},"查找值来自哪里",[308,14371,14372],{},"典型场景",[315,14374,14375,14389],{},[305,14376,14377,14381,14384],{},[320,14378,14379],{},[22,14380,13474],{},[320,14382,14383],{},"SQL 中的常量",[320,14385,14386],{},[22,14387,14388],{},"WHERE id = 100",[305,14390,14391,14395,14398],{},[320,14392,14393],{},[22,14394,14209],{},[320,14396,14397],{},"前面表的列值",[320,14399,14400],{},[22,14401,14402],{},"u.id = o.user_id",[473,14404,5089,14406,14408],{"id":14405},"_5-ref通过非唯一索引等值查找一组记录",[22,14407,13825],{},"：通过非唯一索引等值查找一组记录",[18,14410,14411,14413],{},[22,14412,13825],{}," 表示使用普通索引或联合索引的非唯一前缀进行等值查找，可能返回多条具有相同索引值的记录。",[18,14415,686],{},[144,14417,14419],{"className":8679,"code":14418,"language":8681,"meta":150,"style":150},"KEY idx_status (status)\n",[22,14420,14421],{"__ignoreMap":150},[203,14422,14423,14425,14427,14429],{"class":205,"line":206},[203,14424,11366],{"class":209},[203,14426,12548],{"class":219},[203,14428,12551],{"class":209},[203,14430,12343],{"class":219},[144,14432,14433],{"className":8679,"code":12979,"language":8681,"meta":150,"style":150},[22,14434,14435,14441,14447],{"__ignoreMap":150},[203,14436,14437,14439],{"class":205,"line":206},[203,14438,8365],{"class":209},[203,14440,10532],{"class":209},[203,14442,14443,14445],{"class":205,"line":251},[203,14444,10537],{"class":209},[203,14446,8862],{"class":219},[203,14448,14449,14451,14453,14455,14457],{"class":205,"line":260},[203,14450,8740],{"class":209},[203,14452,9511],{"class":209},[203,14454,9514],{"class":209},[203,14456,11601],{"class":718},[203,14458,1297],{"class":219},[18,14460,4262,14461,14464],{},[22,14462,14463],{},"status=1"," 对应多条订单，访问过程是：",[144,14466,14469],{"className":14467,"code":14468,"language":149,"meta":150},[147],"在 idx_status 中定位 status=1 的起点\n                  ↓\n连续读取所有 status=1 的索引记录\n                  ↓\n按需回表\n",[22,14470,14468],{"__ignoreMap":150},[18,14472,14473,14474,14476],{},"因此，",[22,14475,13825],{}," 不代表只读取一行。它可能读取几行，也可能读取几十万行，实际成本取决于索引选择性和回表数量。",[18,14478,14479,14480,431],{},"联合索引只使用左侧一部分时，也可能出现 ",[22,14481,13825],{},[144,14483,14485],{"className":8679,"code":14484,"language":8681,"meta":150,"style":150},"KEY idx_tenant_status (tenant_id, status)\n",[22,14486,14487],{"__ignoreMap":150},[203,14488,14489,14491,14494,14496],{"class":205,"line":206},[203,14490,11366],{"class":209},[203,14492,14493],{"class":219}," idx_tenant_status (tenant_id, ",[203,14495,12551],{"class":209},[203,14497,12343],{"class":219},[144,14499,14500],{"className":8679,"code":14175,"language":8681,"meta":150,"style":150},[22,14501,14502],{"__ignoreMap":150},[203,14503,14504,14506,14508,14510],{"class":205,"line":206},[203,14505,8740],{"class":209},[203,14507,14154],{"class":219},[203,14509,495],{"class":209},[203,14511,11472],{"class":718},[18,14513,14514],{},"虽然使用了索引最左列，但同一个租户可能有大量记录，所以仍然是非唯一匹配。",[18,14516,14517,14518,431],{},"JOIN 中如果使用普通索引关联，也常见 ",[22,14519,13825],{},[144,14521,14523],{"className":8679,"code":14522,"language":8681,"meta":150,"style":150},"SELECT *\nFROM users u\nJOIN orders o ON o.user_id = u.id;\n",[22,14524,14525,14531,14538],{"__ignoreMap":150},[203,14526,14527,14529],{"class":205,"line":206},[203,14528,8365],{"class":209},[203,14530,10532],{"class":209},[203,14532,14533,14535],{"class":205,"line":251},[203,14534,10537],{"class":209},[203,14536,14537],{"class":219}," users u\n",[203,14539,14540,14542,14545,14547,14549,14551,14553,14555,14557,14559,14561],{"class":205,"line":260},[203,14541,13415],{"class":209},[203,14543,14544],{"class":219}," orders o ",[203,14546,9565],{"class":209},[203,14548,13389],{"class":718},[203,14550,9766],{"class":219},[203,14552,12437],{"class":718},[203,14554,9514],{"class":209},[203,14556,13423],{"class":718},[203,14558,9766],{"class":219},[203,14560,9155],{"class":718},[203,14562,1297],{"class":219},[18,14564,14565,14566,14569,14570,14572,14573,14575,14576,33],{},"对于每个用户，普通索引 ",[22,14567,14568],{},"orders.idx_user_id"," 可能匹配多条订单，因此 ",[22,14571,13470],{}," 的访问类型可能是 ",[22,14574,13825],{},"，而不是 ",[22,14577,14209],{},[473,14579,14581,14582,14584],{"id":14580},"_6-range扫描索引中的一个或多个区间","6. ",[22,14583,13828],{},"：扫描索引中的一个或多个区间",[18,14586,14587,14589],{},[22,14588,13828],{}," 表示 MySQL 先根据条件确定一个或多个索引区间，然后只扫描这些区间中的记录。",[18,14591,14592],{},"典型范围查询：",[144,14594,14596],{"className":8679,"code":14595,"language":8681,"meta":150,"style":150},"SELECT *\nFROM orders\nWHERE created_at >= '2026-07-01'\n  AND created_at \u003C  '2026-08-01';\n",[22,14597,14598,14604,14610,14620],{"__ignoreMap":150},[203,14599,14600,14602],{"class":205,"line":206},[203,14601,8365],{"class":209},[203,14603,10532],{"class":209},[203,14605,14606,14608],{"class":205,"line":251},[203,14607,10537],{"class":209},[203,14609,8862],{"class":219},[203,14611,14612,14614,14616,14618],{"class":205,"line":260},[203,14613,8740],{"class":209},[203,14615,11396],{"class":219},[203,14617,10594],{"class":209},[203,14619,13452],{"class":223},[203,14621,14622,14624,14626,14628,14631],{"class":205,"line":278},[203,14623,9508],{"class":209},[203,14625,11396],{"class":219},[203,14627,3306],{"class":209},[203,14629,14630],{"class":223},"  '2026-08-01'",[203,14632,1297],{"class":219},[18,14634,14635],{},"索引访问过程可以理解为：",[144,14637,14640],{"className":14638,"code":14639,"language":149,"meta":150},[147],"定位 2026-07-01 的索引位置\n              ↓\n沿叶子节点向后扫描\n              ↓\n到达 2026-08-01 时停止\n",[22,14641,14639],{"__ignoreMap":150},[18,14643,14644,14645,14647],{},"常见能够形成 ",[22,14646,13828],{}," 的条件包括：",[159,14649,14650,14660,14664,14670,14674,14679],{},[162,14651,14652,53,14654,53,14656,53,14658,2365],{},[22,14653,9371],{},[22,14655,10594],{},[22,14657,3306],{},[22,14659,10599],{},[162,14661,14662,2365],{},[22,14663,8746],{},[162,14665,14666,14669],{},[22,14667,14668],{},"IN (...)"," 形成多个离散范围；",[162,14671,14672,2365],{},[22,14673,12968],{},[162,14675,14676,14677,2365],{},"能确定前缀范围的 ",[22,14678,10609],{},[162,14680,14681,14682,14685],{},"某些 ",[22,14683,14684],{},"\u003C>"," 或其他可转化为索引区间的条件。",[18,14687,686],{},[144,14689,14691],{"className":8679,"code":14690,"language":8681,"meta":150,"style":150},"WHERE id IN (10, 20, 30)\n",[22,14692,14693],{"__ignoreMap":150},[203,14694,14695,14697,14699,14701,14703,14705,14707,14709,14711,14714],{"class":205,"line":206},[203,14696,8740],{"class":209},[203,14698,9137],{"class":219},[203,14700,11819],{"class":209},[203,14702,5163],{"class":219},[203,14704,2461],{"class":718},[203,14706,5179],{"class":219},[203,14708,10208],{"class":718},[203,14710,5179],{"class":219},[203,14712,14713],{"class":718},"30",[203,14715,12343],{"class":219},[18,14717,14718,14719,14721,14722,33],{},"虽然使用的是多个等值条件，优化器也可能把它组织成多个索引范围，因此 ",[22,14720,11318],{}," 显示为 ",[22,14723,13828],{},[18,14725,14726],{},"具体来说，范围优化器可以把：",[144,14728,14729],{"className":8679,"code":14690,"language":8681,"meta":150,"style":150},[22,14730,14731],{"__ignoreMap":150},[203,14732,14733,14735,14737,14739,14741,14743,14745,14747,14749,14751],{"class":205,"line":206},[203,14734,8740],{"class":209},[203,14736,9137],{"class":219},[203,14738,11819],{"class":209},[203,14740,5163],{"class":219},[203,14742,2461],{"class":718},[203,14744,5179],{"class":219},[203,14746,10208],{"class":718},[203,14748,5179],{"class":219},[203,14750,14713],{"class":718},[203,14752,12343],{"class":219},[18,14754,14755],{},"转换为同一个索引上的三个单点区间：",[144,14757,14760],{"className":14758,"code":14759,"language":149,"meta":150},[147],"[10, 10]\n∪\n[20, 20]\n∪\n[30, 30]\n",[22,14761,14759],{"__ignoreMap":150},[18,14763,14764,14765,14767,14768,14770],{},"执行时并不是从 ",[22,14766,2461],{}," 一直扫描到 ",[22,14769,14713],{},"，而是分别在同一棵 B+ 树中定位这些单点范围，再合并访问结果：",[144,14772,14775],{"className":14773,"code":14774,"language":149,"meta":150},[147],"定位 id=10\n定位 id=20\n定位 id=30\n     ↓\n返回三个范围的记录\n",[22,14776,14774],{"__ignoreMap":150},[18,14778,14779,14780,14782,14783,33],{},"因此，SQL 语义上虽然是多个等值条件，物理访问方式仍属于“扫描一个或多个索引范围”，",[22,14781,11318],{}," 可以显示为 ",[22,14784,13828],{},[18,14786,14787],{},"这与 Index Merge 不同：",[159,14789,14790,14800],{},[162,14791,14792,14795,14796,14799],{},[22,14793,14794],{},"IN (10,20,30)"," 通常是在",[110,14797,14798],{},"同一个索引","上构造多个范围；",[162,14801,14802,14803,14806],{},"Index Merge 是扫描",[110,14804,14805],{},"同一张表的多个索引","，再合并结果。",[18,14808,14809,14811],{},[22,14810,13828],{}," 是否高效，主要看范围大小：",[144,14813,14816],{"className":14814,"code":14815,"language":149,"meta":150},[147],"扫描 10 条索引记录       → 通常很快\n扫描 80 万条索引记录     → 仍可能很慢\n",[22,14817,14815],{"__ignoreMap":150},[18,14819,14820,14821,14823],{},"所以不能看到 ",[22,14822,13828],{}," 就直接认定 SQL 已经优化完成。",[473,14825,14827],{"id":14826},"_7-icp在回表前用索引中的字段过滤","7. ICP：在回表前用索引中的字段过滤",[18,14829,14830],{},"ICP 全称是 Index Condition Pushdown，中文通常称为“索引条件下推”。",[18,14832,14833],{},"仍然使用联合索引：",[144,14835,14836],{"className":8679,"code":11418,"language":8681,"meta":150,"style":150},[22,14837,14838],{"__ignoreMap":150},[203,14839,14840,14842],{"class":205,"line":206},[203,14841,11366],{"class":209},[203,14843,11427],{"class":219},[18,14845,11430],{},[144,14847,14848],{"className":8679,"code":11433,"language":8681,"meta":150,"style":150},[22,14849,14850,14856,14862,14872,14882],{"__ignoreMap":150},[203,14851,14852,14854],{"class":205,"line":206},[203,14853,8365],{"class":209},[203,14855,10532],{"class":209},[203,14857,14858,14860],{"class":205,"line":251},[203,14859,10537],{"class":209},[203,14861,11448],{"class":219},[203,14863,14864,14866,14868,14870],{"class":205,"line":260},[203,14865,8740],{"class":209},[203,14867,11455],{"class":219},[203,14869,495],{"class":209},[203,14871,11460],{"class":718},[203,14873,14874,14876,14878,14880],{"class":205,"line":278},[203,14875,9508],{"class":209},[203,14877,11467],{"class":219},[203,14879,9371],{"class":209},[203,14881,11472],{"class":718},[203,14883,14884,14886,14888,14890,14892],{"class":205,"line":285},[203,14885,9508],{"class":209},[203,14887,11479],{"class":219},[203,14889,495],{"class":209},[203,14891,11484],{"class":718},[203,14893,1297],{"class":219},[18,14895,14896,14898,14899,14901],{},[22,14897,11492],{}," 可以确定索引扫描范围，但 ",[22,14900,11500],{}," 通常不能继续缩小这个连续范围。",[18,14903,14904],{},"没有 ICP 时，过程近似为：",[144,14906,14909],{"className":14907,"code":14908,"language":149,"meta":150},[147],"扫描满足 a=1、b>10 的二级索引记录\n                ↓\n每一条都根据主键回表\n                ↓\n取得完整行后判断 c=3\n",[22,14910,14908],{"__ignoreMap":150},[18,14912,14913,14914,14916,14917,431],{},"开启 ICP 后，由于 ",[22,14915,11247],{}," 也存储在联合索引中，存储引擎可以先检查索引记录中的 ",[22,14918,11247],{},[144,14920,14923],{"className":14921,"code":14922,"language":149,"meta":150},[147],"扫描满足 a=1、b>10 的二级索引记录\n                ↓\n直接在索引记录中判断 c=3\n        ├─ 不满足：跳过，不回表\n        └─ 满足：根据主键回表\n",[22,14924,14922],{"__ignoreMap":150},[18,14926,14927],{},"它的主要收益是：",[105,14929,14930],{},[18,14931,14932],{},"减少不必要的聚簇索引回表，而不是进一步缩小最初的索引扫描范围。",[18,14934,12606,14935,13665,14937,14939],{},[22,14936,11307],{},[22,14938,13668],{}," 中通常显示：",[144,14941,14944],{"className":14942,"code":14943,"language":149,"meta":150},[147],"Using index condition\n",[22,14945,14943],{"__ignoreMap":150},[18,14947,14948],{},"ICP 与覆盖索引也不同：",[159,14950,14951,14954],{},[162,14952,14953],{},"ICP：仍可能需要回表，只是先在索引层过滤；",[162,14955,14956,14957,14959,14960,33],{},"覆盖索引：查询所需数据全部在索引中，最终也不需要回表，",[22,14958,13668],{}," 常见 ",[22,14961,14962],{},"Using index",[18,14964,14965],{},"只有能够利用当前索引记录判断的条件，才适合下推到存储引擎；依赖其他表、存储函数或无法从索引记录取得的数据，通常不能这样处理。",[473,14967,14969,14970,14972],{"id":14968},"_8-index把整棵索引扫描一遍","8. ",[22,14971,12426],{},"：把整棵索引扫描一遍",[18,14974,14975,14977],{},[22,14976,12426],{}," 很容易被误解为“查询已经通过索引精准定位”。",[18,14979,14980,14981,14984],{},"实际上，",[22,14982,14983],{},"type=index"," 通常表示：",[105,14986,14987],{},[18,14988,14989],{},"MySQL 没有通过索引条件缩小扫描区间，而是把选中的整棵索引从头到尾扫描一遍。",[18,14991,14992,14993,14995],{},"它与 ",[22,14994,13833],{}," 的共同点是都可能读取全部记录；区别在于：",[159,14997,14998,15003],{},[162,14999,15000,15002],{},[22,15001,12426],{}," 扫描索引树；",[162,15004,15005,15007],{},[22,15006,13833],{}," 扫描表数据。",[18,15009,686],{},[144,15011,15013],{"className":8679,"code":15012,"language":8681,"meta":150,"style":150},"KEY idx_user_id (user_id)\n",[22,15014,15015],{"__ignoreMap":150},[203,15016,15017,15019],{"class":205,"line":206},[203,15018,11366],{"class":209},[203,15020,14270],{"class":219},[144,15022,15024],{"className":8679,"code":15023,"language":8681,"meta":150,"style":150},"SELECT user_id\nFROM orders;\n",[22,15025,15026,15033],{"__ignoreMap":150},[203,15027,15028,15030],{"class":205,"line":206},[203,15029,8365],{"class":209},[203,15031,15032],{"class":219}," user_id\n",[203,15034,15035,15037],{"class":205,"line":251},[203,15036,10537],{"class":209},[203,15038,13720],{"class":219},[18,15040,15041,15042,15044,15045,15048],{},"如果查询只需要 ",[22,15043,12437],{},"，二级索引已经包含全部所需数据，MySQL 可以扫描更窄的 ",[22,15046,15047],{},"idx_user_id","，而不必扫描保存完整行的聚簇索引。",[18,15050,15051],{},"执行计划可能显示：",[144,15053,15056],{"className":15054,"code":15055,"language":149,"meta":150},[147],"type: index\nkey: idx_user_id\nExtra: Using index\n",[22,15057,15055],{"__ignoreMap":150},[18,15059,8521,15060,15062],{},[22,15061,14962],{}," 表示覆盖索引：查询需要的数据可以直接从索引取得，不需要回表。",[18,15064,15065],{},"另一个常见场景是利用索引顺序避免额外排序：",[144,15067,15069],{"className":8679,"code":15068,"language":8681,"meta":150,"style":150},"SELECT id\nFROM orders\nORDER BY created_at;\n",[22,15070,15071,15077,15083],{"__ignoreMap":150},[203,15072,15073,15075],{"class":205,"line":206},[203,15074,8365],{"class":209},[203,15076,10937],{"class":219},[203,15078,15079,15081],{"class":205,"line":251},[203,15080,10537],{"class":209},[203,15082,8862],{"class":219},[203,15084,15085,15087],{"class":205,"line":260},[203,15086,10615],{"class":209},[203,15088,13105],{"class":219},[18,15090,15091,15092,15094,15095,33],{},"如果存在适合的 ",[22,15093,11832],{}," 索引，优化器可能按索引顺序扫描全部记录，以避免 ",[22,15096,13310],{},[18,15098,15099,15101,15102,15104],{},[22,15100,12426],{}," 可能比 ",[22,15103,13833],{}," 快，因为二级索引通常更窄，缓存命中和读取的数据量更小；但它本质上仍是全索引扫描，数据量大时成本依然很高。",[473,15106,15108,15109,15111],{"id":15107},"_9-all扫描整张表","9. ",[22,15110,13833],{},"：扫描整张表",[18,15113,15114,15116],{},[22,15115,13833],{}," 表示全表扫描。",[18,15118,15119],{},"对于 InnoDB，可以把它近似理解为扫描保存完整行数据的聚簇索引：",[144,15121,15124],{"className":15122,"code":15123,"language":149,"meta":150},[147],"聚簇索引第一条记录\n        ↓\n逐条向后读取\n        ↓\n检查 WHERE 条件\n        ↓\n直到最后一条记录\n",[22,15125,15123],{"__ignoreMap":150},[18,15127,15128],{},"典型原因包括：",[159,15130,15131,15134,15137,15141,15144,15147],{},[162,15132,15133],{},"查询条件没有可用索引；",[162,15135,15136],{},"在索引列上进行了导致无法定位的函数或转换；",[162,15138,15139,11274],{},[22,15140,10729],{},[162,15142,15143],{},"条件会返回表中大部分数据；",[162,15145,15146],{},"表很小，全表扫描成本更低；",[162,15148,15149],{},"优化器根据统计信息判断使用索引加回表更贵。",[18,15151,686],{},[144,15153,15155],{"className":8679,"code":15154,"language":8681,"meta":150,"style":150},"SELECT *\nFROM orders\nWHERE remark LIKE '%manual%';\n",[22,15156,15157,15163,15169],{"__ignoreMap":150},[203,15158,15159,15161],{"class":205,"line":206},[203,15160,8365],{"class":209},[203,15162,10532],{"class":209},[203,15164,15165,15167],{"class":205,"line":251},[203,15166,10537],{"class":209},[203,15168,8862],{"class":219},[203,15170,15171,15173,15175,15177,15180],{"class":205,"line":260},[203,15172,8740],{"class":209},[203,15174,12473],{"class":219},[203,15176,10729],{"class":209},[203,15178,15179],{"class":223}," '%manual%'",[203,15181,1297],{"class":219},[18,15183,15184,15185,33],{},"普通 B+ 树索引无法按照任意位置包含关系定位，执行计划很可能是 ",[22,15186,13833],{},[18,15188,15189,15190,15192],{},"但 ",[22,15191,13833],{}," 不等于必须立刻创建索引。",[18,15194,15195],{},"如果表只有几十行，或者查询本来就要读取 80% 的数据，全表扫描可能是合理选择。真正需要关注的是：",[159,15197,15198,15201,15206,15209,15212],{},[162,15199,15200],{},"表有多大；",[162,15202,15203,15205],{},[22,15204,13840],{}," 预计扫描多少行；",[162,15207,15208],{},"查询执行频率多高；",[162,15210,15211],{},"是否处于 JOIN 的内层并被重复扫描；",[162,15213,15214],{},"实际耗时和资源消耗是否不可接受。",[18,15216,15217,15218,15220],{},"特别是在 JOIN 中，如果前表返回一万行，而后表对每条前表记录都执行一次 ",[22,15219,13833],{},"，成本可能被放大得非常严重。",[473,15222,15224],{"id":15223},"_10-常见类型对比","10. 常见类型对比",[299,15226,15227,15246],{},[302,15228,15229],{},[305,15230,15231,15235,15238,15241,15244],{},[308,15232,15233],{},[22,15234,11318],{},[308,15236,15237],{},"核心含义",[308,15239,15240],{"align":8425},"是否精准定位",[308,15242,15243],{"align":8425},"可能读取的记录数",[308,15245,609],{},[315,15247,15248,15266,15285,15305,15328,15345],{},[305,15249,15250,15254,15257,15260,15263],{},[320,15251,15252],{},[22,15253,13946],{},[320,15255,15256],{},"表中只有一行",[320,15258,15259],{"align":8425},"无需查找",[320,15261,15262],{"align":8425},"1 行",[320,15264,15265],{},"单行表",[305,15267,15268,15272,15275,15277,15280],{},[320,15269,15270],{},[22,15271,13474],{},[320,15273,15274],{},"通过常量匹配主键或唯一键",[320,15276,981],{"align":8425},[320,15278,15279],{"align":8425},"最多 1 行",[320,15281,15282],{},[22,15283,15284],{},"WHERE id=100",[305,15286,15287,15291,15294,15297,15300],{},[320,15288,15289],{},[22,15290,13825],{},[320,15292,15293],{},"通过普通索引做等值查找",[320,15295,15296],{"align":8425},"是，但结果不唯一",[320,15298,15299],{"align":8425},"1 行到大量记录",[320,15301,15302],{},[22,15303,15304],{},"WHERE status=1",[305,15306,15307,15311,15314,15317,15320],{},[320,15308,15309],{},[22,15310,13828],{},[320,15312,15313],{},"扫描一个或多个索引区间",[320,15315,15316],{"align":8425},"定位区间",[320,15318,15319],{"align":8425},"取决于范围大小",[320,15321,15322,53,15324,53,15326],{},[22,15323,8746],{},[22,15325,11819],{},[22,15327,9371],{},[305,15329,15330,15334,15337,15339,15342],{},[320,15331,15332],{},[22,15333,12426],{},[320,15335,15336],{},"扫描整棵索引",[320,15338,984],{"align":8425},[320,15340,15341],{"align":8425},"通常为整个索引",[320,15343,15344],{},"覆盖索引全扫描、按索引顺序遍历",[305,15346,15347,15351,15354,15356,15359],{},[320,15348,15349],{},[22,15350,13833],{},[320,15352,15353],{},"扫描整张表",[320,15355,984],{"align":8425},[320,15357,15358],{"align":8425},"通常为整张表",[320,15360,15361],{},"无可用索引或全表扫描更便宜",[18,15363,15364,15365,431],{},"还应单独记住 JOIN 中的 ",[22,15366,14209],{},[105,15368,15369],{},[18,15370,15371],{},"对前表的每一条记录，通过当前表的主键或唯一非空索引最多匹配一行。",[473,15373,15375],{"id":15374},"_11-explain-与-explain-analyze-的区别","11. EXPLAIN 与 EXPLAIN ANALYZE 的区别",[18,15377,15378,15380,15381,431],{},[22,15379,11307],{}," 只展示优化器计划如何执行 SQL，并不会真正执行普通 ",[22,15382,8365],{},[144,15384,15385],{"className":8679,"code":13736,"language":8681,"meta":150,"style":150},[22,15386,15387,15391,15397,15403],{"__ignoreMap":150},[203,15388,15389],{"class":205,"line":206},[203,15390,13743],{"class":219},[203,15392,15393,15395],{"class":205,"line":251},[203,15394,8365],{"class":209},[203,15396,10532],{"class":209},[203,15398,15399,15401],{"class":205,"line":260},[203,15400,10537],{"class":209},[203,15402,8862],{"class":219},[203,15404,15405,15407,15409,15411,15413],{"class":205,"line":278},[203,15406,8740],{"class":209},[203,15408,9499],{"class":219},[203,15410,495],{"class":209},[203,15412,8749],{"class":718},[203,15414,1297],{"class":219},[18,15416,15417,15418,15421,15422,33],{},"其中的 ",[22,15419,15420],{},"rows、filtered、cost"," 等主要来自统计信息和成本模型，是",[110,15423,15424],{},"估算值",[18,15426,15427,15429],{},[22,15428,11310],{}," 会真正执行查询，并在执行计划中同时展示估算值和实际运行数据：",[144,15431,15433],{"className":8679,"code":15432,"language":8681,"meta":150,"style":150},"EXPLAIN ANALYZE\nSELECT *\nFROM orders\nWHERE user_id = 100;\n",[22,15434,15435,15440,15446,15452],{"__ignoreMap":150},[203,15436,15437],{"class":205,"line":206},[203,15438,15439],{"class":219},"EXPLAIN ANALYZE\n",[203,15441,15442,15444],{"class":205,"line":251},[203,15443,8365],{"class":209},[203,15445,10532],{"class":209},[203,15447,15448,15450],{"class":205,"line":260},[203,15449,10537],{"class":209},[203,15451,8862],{"class":219},[203,15453,15454,15456,15458,15460,15462],{"class":205,"line":278},[203,15455,8740],{"class":209},[203,15457,9499],{"class":219},[203,15459,495],{"class":209},[203,15461,8749],{"class":718},[203,15463,1297],{"class":219},[18,15465,15466],{},"典型输出片段：",[144,15468,15471],{"className":15469,"code":15470,"language":149,"meta":150},[147],"Index lookup on orders using idx_user_id\n  (cost=10.2 rows=20)\n  (actual time=0.08..0.15 rows=350 loops=1)\n",[22,15472,15470],{"__ignoreMap":150},[18,15474,15475],{},"可以重点对比：",[159,15477,15478,15484,15490,15496,15502],{},[162,15479,15480,15483],{},[22,15481,15482],{},"cost","：优化器估算成本；",[162,15485,15486,15487,15489],{},"前一个 ",[22,15488,13840],{},"：预计返回行数；",[162,15491,15492,15495],{},[22,15493,15494],{},"actual time","：实际首行时间和完成时间；",[162,15497,15498,15499,15501],{},"后一个 ",[22,15500,13840],{},"：每次循环实际返回行数；",[162,15503,15504,15507],{},[22,15505,15506],{},"loops","：该执行节点实际执行次数。",[18,15509,15510],{},"两者的核心区别：",[299,15512,15513,15527],{},[302,15514,15515],{},[305,15516,15517,15519,15523],{},[308,15518,950],{},[308,15520,15521],{},[22,15522,11307],{},[308,15524,15525],{},[22,15526,11310],{},[315,15528,15529,15542,15553,15564,15575],{},[305,15530,15531,15534,15539],{},[320,15532,15533],{},"是否真正执行查询",[320,15535,8362,15536,15538],{},[22,15537,8365],{}," 不执行",[320,15540,15541],{},"会真实执行",[305,15543,15544,15547,15550],{},[320,15545,15546],{},"行数与成本",[320,15548,15549],{},"主要是估算值",[320,15551,15552],{},"同时提供估算与实际数据",[305,15554,15555,15558,15561],{},[320,15556,15557],{},"实际耗时",[320,15559,15560],{},"不提供",[320,15562,15563],{},"提供各执行节点耗时",[305,15565,15566,15569,15572],{},[320,15567,15568],{},"主要用途",[320,15570,15571],{},"上线前检查计划、低风险分析",[320,15573,15574],{},"验证估算偏差、定位真实瓶颈",[305,15576,15577,15580,15583],{},[320,15578,15579],{},"使用风险",[320,15581,15582],{},"较低",[320,15584,15585],{},"慢 SQL 会真实消耗资源",[18,15587,15588,15590],{},[22,15589,11310],{}," 能发现这类问题：",[144,15592,15595],{"className":15593,"code":15594,"language":149,"meta":150},[147],"优化器预计返回 20 行\n实际返回 350000 行\n",[22,15596,15594],{"__ignoreMap":150},[18,15598,15599],{},"这种巨大偏差可能与统计信息、数据倾斜、条件相关性或估算模型有关。",[18,15601,15602],{},"使用时必须注意：",[159,15604,15605,15608,15611,15614],{},[162,15606,15607],{},"它会真实执行查询，慢查询会真实占用 CPU、I\u002FO 和锁资源；",[162,15609,15610],{},"对复杂 SQL 应优先在测试或影子环境执行；",[162,15612,15613],{},"不要在生产高峰期随意分析可能扫描海量数据的语句；",[162,15615,15616],{},"对数据修改语句尤其谨慎，不能把它当成完全无副作用的静态分析工具。",[473,15618,15620,15621,15623],{"id":15619},"_12-不要只根据-type-判断性能","12. 不要只根据 ",[22,15622,11318],{}," 判断性能",[18,15625,15626],{},"下面两个计划：",[144,15628,15631],{"className":15629,"code":15630,"language":149,"meta":150},[147],"计划 A：type=ref，rows=500000\n计划 B：type=range，rows=20\n",[22,15632,15630],{"__ignoreMap":150},[18,15634,15635,15636,15638,15639,15641],{},"虽然官方列表中 ",[22,15637,13825],{}," 排在 ",[22,15640,13828],{}," 前面，但计划 B 很可能扫描更少、执行更快。",[18,15643,15644],{},"同理：",[144,15646,15649],{"className":15647,"code":15648,"language":149,"meta":150},[147],"计划 A：type=ALL，rows=30\n计划 B：type=ref，rows=100000\n",[22,15650,15648],{"__ignoreMap":150},[18,15652,15653],{},"对只有 30 行的小表执行全表扫描，不一定是问题。",[18,15655,15656],{},"因此，合理的判断方式是：",[144,15658,15661],{"className":15659,"code":15660,"language":149,"meta":150},[147],"type\n  +\nkey \u002F key_len\n  +\nrows \u002F filtered\n  +\nExtra\n  +\nEXPLAIN ANALYZE 的实际行数与耗时\n",[22,15662,15660],{"__ignoreMap":150},[18,15664,15665,15667],{},[22,15666,11318],{}," 是执行计划的重要入口，但不是最终结论。",[18,15669,15670],{},"几个常见误区：",[159,15672,15673,15678,15683,15688,15694,15699],{},[162,15674,15675,15677],{},[22,15676,11303],{}," 有值，不代表最终使用了索引；",[162,15679,15680,15682],{},[22,15681,11314],{}," 有值，不代表查询一定高效；",[162,15684,15685,15687],{},[22,15686,14983],{}," 通常表示全索引扫描，不等于范围定位；",[162,15689,15690,15693],{},[22,15691,15692],{},"type=ALL"," 是全表扫描，但小表全表扫描可能就是合理计划；",[162,15695,15696,15698],{},[22,15697,14962],{}," 通常表示覆盖索引，不等于“使用索引定位”；",[162,15700,15701],{},"预计扫描行数少，也可能因估算偏差与实际执行差异很大。",[14,15703,15705],{"id":15704},"十三排查顺序","十三、排查顺序",[18,15707,15708],{},"遇到“明明建了索引却很慢”，可以按下面顺序检查：",[2695,15710,15711,15714,15717,15732,15735,15738,15741,15744,15750],{},[162,15712,15713],{},"确认慢 SQL、参数值和数据量，不能只拿脱离现场的 SQL 模板分析。",[162,15715,15716],{},"查看表结构、索引顺序、字段类型、字符集和排序规则。",[162,15718,4104,15719,15721,15722,53,15724,53,15726,53,15728,40,15730,33],{},[22,15720,11307],{}," 检查 ",[22,15723,11314],{},[22,15725,11318],{},[22,15727,13840],{},[22,15729,13850],{},[22,15731,13668],{},[162,15733,15734],{},"检查联合索引是否满足最左前缀，范围条件之后是否只剩过滤能力。",[162,15736,15737],{},"检查索引列上是否存在函数、计算或隐式转换。",[162,15739,15740],{},"估算查询命中比例和回表次数，判断优化器放弃索引是否合理。",[162,15742,15743],{},"对比估算行数与实际行数，检查统计信息是否明显失真。",[162,15745,15746,15747,15749],{},"在可控环境中使用 ",[22,15748,11310],{}," 验证真实执行路径。",[162,15751,15752,15753,15755],{},"调整 SQL 或索引后，在可控环境中使用 ",[22,15754,11310],{}," 验证实际执行情况，并结合线上灰度后的查询耗时、扫描行数、回表量及资源指标持续观察，不能只看静态执行计划就宣布优化完成。只有涉及高并发、容量上限或锁竞争的场景，才需要进一步进行专项压测。",[14,15757,15759],{"id":15758},"十四常见错误结论","十四、常见错误结论",[473,15761,15763],{"id":15762},"_1-联合索引中间断了一列整个索引都失效","1. 联合索引中间断了一列，整个索引都失效",[18,15765,15766],{},"不准确。最左侧连续部分仍可能用于定位，后续列也可能通过 ICP 或覆盖索引发挥作用。",[473,15768,15770],{"id":15769},"_2-遇到范围查询后面的索引列完全没用","2. 遇到范围查询，后面的索引列完全没用",[18,15772,15773],{},"不准确。后续列通常不能继续缩小连续范围，但仍可能用于索引层过滤或覆盖查询。",[473,15775,6437,15777,53,15779,53,15781,15783],{"id":15776},"_3-not-inis-null-一定不走索引",[22,15778,1350],{},[22,15780,11287],{},[22,15782,12968],{}," 一定不走索引",[18,15785,15786],{},"不准确。是否选择索引取决于数据分布、命中比例、覆盖情况和优化器成本估算。",[473,15788,14206,15790,15792],{"id":15789},"_4-key-不为-null-就说明索引使用良好",[22,15791,11314],{}," 不为 NULL 就说明索引使用良好",[18,15794,15795],{},"不准确。可能扫描了大部分索引，也可能发生大量回表。",[473,15797,15799],{"id":15798},"_5-全表扫描一定比索引慢","5. 全表扫描一定比索引慢",[18,15801,15802],{},"不准确。小表或需要读取大部分数据时，顺序全表扫描可能是成本更低的方案。",[473,15804,15806,15807,15809],{"id":15805},"_6-使用-force-index-就能解决问题","6. 使用 ",[22,15808,13045],{}," 就能解决问题",[18,15811,15812],{},"不准确。强制索引可能暂时改变执行计划，也可能在数据分布变化后变得更慢。应先找到优化器不选择索引的真实原因。",[14,15814,2071],{"id":2070},[159,15816,15817,15824,15831,15838,15845,15852,15859,15866],{},[162,15818,15819],{},[119,15820,15823],{"href":15821,"rel":15822},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.0\u002Fen\u002Fmysql-indexes.html",[2080],"MySQL 8.0 Reference Manual：How MySQL Uses Indexes",[162,15825,15826],{},[119,15827,15830],{"href":15828,"rel":15829},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.0\u002Fen\u002Foptimization-indexes.html",[2080],"MySQL 8.0 Reference Manual：Optimization and Indexes",[162,15832,15833],{},[119,15834,15837],{"href":15835,"rel":15836},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.0\u002Fen\u002Fexplain.html",[2080],"MySQL 8.0 Reference Manual：EXPLAIN Statement",[162,15839,15840],{},[119,15841,15844],{"href":15842,"rel":15843},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.0\u002Fen\u002Findex-merge-optimization.html",[2080],"MySQL 8.0 Reference Manual：Index Merge Optimization",[162,15846,15847],{},[119,15848,15851],{"href":15849,"rel":15850},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.0\u002Fen\u002Findex-condition-pushdown-optimization.html",[2080],"MySQL 8.0 Reference Manual：Index Condition Pushdown Optimization",[162,15853,15854],{},[119,15855,15858],{"href":15856,"rel":15857},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.0\u002Fen\u002Forder-by-optimization.html",[2080],"MySQL 8.0 Reference Manual：ORDER BY Optimization",[162,15860,15861],{},[119,15862,15865],{"href":15863,"rel":15864},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.0\u002Fen\u002Fcreate-index.html",[2080],"MySQL 8.0 Reference Manual：CREATE INDEX Statement",[162,15867,15868],{},[119,15869,15872],{"href":15870,"rel":15871},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.0\u002Fen\u002Fgenerated-column-index-optimizations.html",[2080],"MySQL 8.0 Reference Manual：Optimizer Use of Generated Column Indexes",[2111,15874,15875],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}",{"title":150,"searchDepth":251,"depth":251,"links":15877},[15878,15881,15882,15883,15888,15889,15890,15891,15892,15893,15899,15900,15901,15906,15907,15930,15931,15942],{"id":16,"depth":251,"text":16,"children":15879},[15880],{"id":8377,"depth":260,"text":8377},{"id":115,"depth":251,"text":115},{"id":132,"depth":251,"text":132},{"id":11342,"depth":251,"text":11343,"children":15884},[15885,15886,15887],{"id":11349,"depth":260,"text":11350},{"id":11409,"depth":260,"text":11410},{"id":11513,"depth":260,"text":11514},{"id":11532,"depth":251,"text":11533},{"id":11724,"depth":251,"text":11725},{"id":11826,"depth":251,"text":11827},{"id":12218,"depth":251,"text":12219},{"id":12323,"depth":251,"text":12324},{"id":12430,"depth":251,"text":12431,"children":15894},[15895,15896,15898],{"id":12518,"depth":260,"text":12519},{"id":12685,"depth":260,"text":15897},"Using sort_union(...) 是什么意思",{"id":12816,"depth":260,"text":12817},{"id":12889,"depth":251,"text":12890},{"id":12972,"depth":251,"text":12973},{"id":13049,"depth":251,"text":13050,"children":15902},[15903,15904,15905],{"id":13168,"depth":260,"text":13168},{"id":13374,"depth":260,"text":13374},{"id":13511,"depth":260,"text":13511},{"id":13680,"depth":251,"text":13681},{"id":13729,"depth":251,"text":13730,"children":15908},[15909,15911,15913,15915,15917,15919,15921,15922,15924,15926,15927,15928],{"id":13865,"depth":260,"text":15910},"1. type 到底表示什么",{"id":13942,"depth":260,"text":15912},"2. system：表中只有一行",{"id":13972,"depth":260,"text":15914},"3. const：通过主键或唯一键最多定位一行",{"id":14205,"depth":260,"text":15916},"4. eq_ref：JOIN 时每条前表记录最多匹配一行",{"id":14405,"depth":260,"text":15918},"5. ref：通过非唯一索引等值查找一组记录",{"id":14580,"depth":260,"text":15920},"6. range：扫描索引中的一个或多个区间",{"id":14826,"depth":260,"text":14827},{"id":14968,"depth":260,"text":15923},"8. index：把整棵索引扫描一遍",{"id":15107,"depth":260,"text":15925},"9. ALL：扫描整张表",{"id":15223,"depth":260,"text":15224},{"id":15374,"depth":260,"text":15375},{"id":15619,"depth":260,"text":15929},"12. 不要只根据 type 判断性能",{"id":15704,"depth":251,"text":15705},{"id":15758,"depth":251,"text":15759,"children":15932},[15933,15934,15935,15937,15939,15940],{"id":15762,"depth":260,"text":15763},{"id":15769,"depth":260,"text":15770},{"id":15776,"depth":260,"text":15936},"3. !=、NOT IN、IS NULL 一定不走索引",{"id":15789,"depth":260,"text":15938},"4. key 不为 NULL 就说明索引使用良好",{"id":15798,"depth":260,"text":15799},{"id":15805,"depth":260,"text":15941},"6. 使用 FORCE INDEX 就能解决问题",{"id":2070,"depth":251,"text":2071},"wiki:java:mysql-index-failure-scenarios","从联合索引、函数运算、隐式转换、LIKE、OR、选择性和优化器成本等角度，系统解释 MySQL 索引无法使用、部分使用及主动放弃索引的常见场景。",{},77,"\u002Fjava\u002Fmysql-index-failure-scenarios",{"title":11211,"description":15944},"java\u002Fmysql-index-failure-scenarios","Z4hbUCudKpBrIxf2AeaWFne0fjdxJ0cTwR0Q_5IRNh0",{"id":15952,"title":15953,"body":15954,"commentId":17139,"description":17140,"difficulty":4584,"draft":2148,"extension":2149,"meta":17141,"navigation":756,"order":17142,"path":17143,"section":2153,"seo":17144,"stem":17145,"updated":17146,"__hash__":17147},"java\u002Fjava\u002Frocketmq-message-types-and-use-cases.md","RocketMQ 都有哪些消息类型？使用场景是什么？",{"type":7,"value":15955,"toc":17084},[15956,15959,15961,15964,15990,15993,15995,16002,16004,16013,16015,16019,16022,16090,16093,16096,16110,16113,16117,16121,16124,16130,16133,16136,16141,16145,16148,16171,16174,16180,16183,16187,16190,16201,16204,16208,16212,16215,16218,16224,16227,16233,16243,16247,16250,16253,16259,16262,16266,16269,16272,16275,16278,16281,16284,16287,16293,16297,16317,16321,16324,16327,16344,16347,16351,16355,16358,16364,16367,16414,16418,16424,16427,16431,16454,16457,16463,16466,16470,16477,16494,16497,16501,16504,16510,16517,16520,16524,16528,16531,16536,16539,16547,16550,16556,16560,16566,16569,16575,16578,16581,16598,16602,16605,16607,16613,16616,16633,16636,16640,16643,16649,16652,16712,16715,16719,16723,16726,16732,16735,16749,16753,16756,16759,16765,16771,16784,16787,16791,16794,16797,16814,16818,16821,16827,16839,16843,16846,16852,16855,16859,16862,16868,16871,16905,16908,16911,16930,16933,16947,16950,16954,16958,16961,16965,16968,16972,16975,16979,16982,16986,16989,16993,16997,17000,17004,17007,17011,17014,17018,17021,17025,17028,17030,17081],[10,15957,15953],{"id":15958},"rocketmq-都有哪些消息类型使用场景是什么",[14,15960,16],{"id":16},[18,15962,15963],{},"RocketMQ 5.x 按消息的传输语义正式分为四种类型：",[2695,15965,15966,15972,15978,15984],{},[162,15967,15968,15971],{},[110,15969,15970],{},"普通消息 Normal","：没有顺序、定时或事务等特殊语义，发送成功后即可被消费，适合微服务解耦、异步通知、日志和数据集成。",[162,15973,15974,15977],{},[110,15975,15976],{},"顺序消息 FIFO","：同一 Message Group 内的消息按照发送、存储和投递顺序处理，适合同一订单的创建、支付、发货等状态流转，以及 Binlog 增量同步。",[162,15979,15980,15983],{},[110,15981,15982],{},"定时与延时消息 Delay","：消息到指定时间后才对消费者可见，适合订单超时关闭、延迟检查、定时通知和任务调度。",[162,15985,15986,15989],{},[110,15987,15988],{},"事务消息 Transaction","：通过半事务消息、本地事务确认和事务回查，保证本地事务与消息发送的最终一致性，适合下单后发放积分、扣款后发送通知等场景。",[18,15991,15992],{},"RocketMQ 5.x 可以为 Topic 声明消息类型，一个 Topic 只应承载一种消息类型。批量消息属于发送方式，重试消息和死信消息属于消费失败后的系统处理状态，Request-Reply 属于通信模式，它们都不属于上述四种正式 MessageType。",[18,15994,103],{},[105,15996,15997],{},[18,15998,15999],{},[110,16000,16001],{},"无特殊要求用普通消息；同一业务实体需要按序处理用 FIFO；需要未来触发用 Delay；本地事务必须和消息发送保持最终一致用 Transaction。",[14,16003,115],{"id":115},[18,16005,16006],{},[119,16007,16010],{"href":16008,"target":122,"rel":16009},"\u002Fimages\u002Fwiki\u002Fjava\u002Frocketmq-message-types-and-use-cases.svg",[124,125],[127,16011],{"src":16008,"alt":16012},"RocketMQ 四种消息类型、核心语义、典型场景及选择方法",[14,16014,132],{"id":132},[14,16016,16018],{"id":16017},"一先明确-rocketmq-的正式分类","一、先明确 RocketMQ 的正式分类",[18,16020,16021],{},"RocketMQ 5.x 定义了四种 MessageType：",[299,16023,16024,16036],{},[302,16025,16026],{},[305,16027,16028,16031,16034],{},[308,16029,16030],{},"消息类型",[308,16032,16033],{},"核心语义",[308,16035,14372],{},[315,16037,16038,16051,16064,16077],{},[305,16039,16040,16045,16048],{},[320,16041,16042],{},[22,16043,16044],{},"NORMAL",[320,16046,16047],{},"发送后立即进入可消费状态",[320,16049,16050],{},"异步解耦、事件通知、日志传输",[305,16052,16053,16058,16061],{},[320,16054,16055],{},[22,16056,16057],{},"FIFO",[320,16059,16060],{},"同一 Message Group 内按顺序处理",[320,16062,16063],{},"订单状态流转、Binlog 同步",[305,16065,16066,16071,16074],{},[320,16067,16068],{},[22,16069,16070],{},"DELAY",[320,16072,16073],{},"到指定时间后才允许消费",[320,16075,16076],{},"超时关闭、延迟检查、定时任务",[305,16078,16079,16084,16087],{},[320,16080,16081],{},[22,16082,16083],{},"TRANSACTION",[320,16085,16086],{},"本地事务与消息发送最终一致",[320,16088,16089],{},"交易完成后驱动下游业务",[18,16091,16092],{},"这四种类型不是按照消息内容划分，而是按照消息需要具备的传输和投递语义划分。",[18,16094,16095],{},"例如，同样是“订单创建”事件：",[159,16097,16098,16101,16104,16107],{},[162,16099,16100],{},"创建后立即异步通知风控，可以使用普通消息；",[162,16102,16103],{},"必须与后续支付、发货事件保持同一订单内的顺序，可以使用顺序消息；",[162,16105,16106],{},"创建三十分钟后检查是否支付，可以使用延时消息；",[162,16108,16109],{},"只有订单数据库事务成功后才能向下游投递，可以使用事务消息。",[18,16111,16112],{},"因此，消息类型的选择取决于业务语义，而不是消息中装了什么数据。",[14,16114,16116],{"id":16115},"二普通消息-normal","二、普通消息 Normal",[473,16118,16120],{"id":16119},"_1-什么是普通消息","1. 什么是普通消息",[18,16122,16123],{},"普通消息没有顺序、定时和事务等特殊语义。Broker 接收并保存消息后，消息进入 Ready 状态，Consumer 可以正常获取并处理。",[144,16125,16128],{"className":16126,"code":16127,"language":149,"meta":150},[147],"Producer\n   │ 发送\n   ▼\nBroker 持久化\n   │ 立即可见\n   ▼\nConsumer 消费\n",[22,16129,16127],{"__ignoreMap":150},[18,16131,16132],{},"“普通”不代表不可靠。普通消息同样可以使用发送确认、Broker 持久化、消费重试、死信队列和业务幂等机制。",[18,16134,16135],{},"它只表示：",[105,16137,16138],{},[18,16139,16140],{},"消息之间没有需要 RocketMQ 额外维护的顺序、时间或事务关系。",[473,16142,16144],{"id":16143},"_2-适用场景","2. 适用场景",[18,16146,16147],{},"普通消息适合绝大多数异步业务：",[159,16149,16150,16153,16156,16159,16162,16165,16168],{},[162,16151,16152],{},"微服务之间异步解耦；",[162,16154,16155],{},"用户注册后异步发送欢迎短信；",[162,16157,16158],{},"订单创建后异步更新搜索索引；",[162,16160,16161],{},"操作日志、埋点和审计事件传输；",[162,16163,16164],{},"缓存失效通知；",[162,16166,16167],{},"数据采集和数据集成；",[162,16169,16170],{},"不要求严格顺序的业务事件广播。",[18,16172,16173],{},"例如，订单创建后需要通知多个独立系统：",[144,16175,16178],{"className":16176,"code":16177,"language":149,"meta":150},[147],"订单服务\n   │\n   └── OrderCreated 普通消息\n           ├── 风控服务\n           ├── 推荐服务\n           └── 数据分析服务\n",[22,16179,16177],{"__ignoreMap":150},[18,16181,16182],{},"这些下游之间不存在必须先后执行的关系，使用普通消息能够获得更好的并发能力。",[473,16184,16186],{"id":16185},"_3-不适合的场景","3. 不适合的场景",[18,16188,16189],{},"以下要求不能只依赖普通消息：",[159,16191,16192,16195,16198],{},[162,16193,16194],{},"同一订单的支付必须在创建之后处理；",[162,16196,16197],{},"消息必须在未来某个时间触发；",[162,16199,16200],{},"本地数据库事务失败时消息绝对不能对下游可见。",[18,16202,16203],{},"这时应分别考虑 FIFO、Delay 或 Transaction。",[14,16205,16207],{"id":16206},"三顺序消息-fifo","三、顺序消息 FIFO",[473,16209,16211],{"id":16210},"_1-顺序消息保证什么","1. 顺序消息保证什么",[18,16213,16214],{},"RocketMQ 5.x 使用 Message Group 标识需要保持顺序的一组消息。同一个 Message Group 中的消息按照先进先出顺序处理。",[18,16216,16217],{},"例如以订单号作为 Message Group：",[144,16219,16222],{"className":16220,"code":16221,"language":149,"meta":150},[147],"Message Group = order-1001\n\n创建订单 → 支付成功 → 仓库出库 → 物流发货\n",[22,16223,16221],{"__ignoreMap":150},[18,16225,16226],{},"另一个订单可以使用不同的 Message Group：",[144,16228,16231],{"className":16229,"code":16230,"language":149,"meta":150},[147],"order-1001：创建 → 支付 → 发货\norder-1002：创建 → 取消\n",[22,16232,16230],{"__ignoreMap":150},[18,16234,16235,16236,16239,16240,33],{},"两个订单之间不要求顺序，可以并发处理；同一订单内部保持顺序。这通常称为",[110,16237,16238],{},"局部顺序","或",[110,16241,16242],{},"分组顺序",[473,16244,16246],{"id":16245},"_2-为什么不追求整个-topic-的全局顺序","2. 为什么不追求整个 Topic 的全局顺序",[18,16248,16249],{},"如果一个 Topic 的全部消息都要求全局顺序，就只能把所有消息串行发送、存储和消费，并发能力会受到严重限制。",[18,16251,16252],{},"按订单号、用户 ID、账户 ID 等业务键划分 Message Group，可以做到：",[144,16254,16257],{"className":16255,"code":16256,"language":149,"meta":150},[147],"组内有序\n+\n组间并行\n",[22,16258,16256],{"__ignoreMap":150},[18,16260,16261],{},"这更符合大多数业务的真实要求。",[473,16263,16265],{"id":16264},"_3-保证顺序需要满足哪些条件","3. 保证顺序需要满足哪些条件",[18,16267,16268],{},"完整的顺序需要覆盖三个阶段。",[6462,16270,16271],{"id":16271},"发送顺序",[18,16273,16274],{},"同一 Message Group 的消息需要由生产端按业务顺序串行发送。如果多个线程或多个生产者同时发送，即使 Message Group 相同，RocketMQ 也无法推断业务上谁应该在前。",[6462,16276,16277],{"id":16277},"存储顺序",[18,16279,16280],{},"RocketMQ 将同一 Message Group 的消息存入同一个队列，并保持接收顺序。",[6462,16282,16283],{"id":16283},"消费顺序",[18,16285,16286],{},"Consumer 必须遵循“接收、处理、确认”的顺序。不能收到消息后随意提交给异步线程并立即确认，否则业务实际完成顺序仍可能被打乱。",[144,16288,16291],{"className":16289,"code":16290,"language":149,"meta":150},[147],"发送有序\n   ↓\n存储有序\n   ↓\n投递有序\n   ↓\n业务处理有序\n",[22,16292,16290],{"__ignoreMap":150},[473,16294,16296],{"id":16295},"_4-适用场景","4. 适用场景",[159,16298,16299,16302,16305,16308,16311,16314],{},[162,16300,16301],{},"同一订单的创建、支付、取消和发货；",[162,16303,16304],{},"同一账户的充值、扣款和退款；",[162,16306,16307],{},"同一用户的状态变更；",[162,16309,16310],{},"数据库 Binlog 增量同步；",[162,16312,16313],{},"工作流节点推进；",[162,16315,16316],{},"交易撮合等要求按到达顺序处理的场景。",[473,16318,16320],{"id":16319},"_5-使用代价与风险","5. 使用代价与风险",[18,16322,16323],{},"顺序消息会牺牲部分并发能力，并且前面的消息处理失败时，后续消息可能等待。",[18,16325,16326],{},"需要重点关注：",[159,16328,16329,16332,16335,16338,16341],{},[162,16330,16331],{},"Message Group 是否过热；",[162,16333,16334],{},"单条消息是否持续重试并阻塞后续消息；",[162,16336,16337],{},"消费逻辑是否包含慢 SQL、慢 RPC；",[162,16339,16340],{},"是否错误地在消费端进行异步并发处理；",[162,16342,16343],{},"是否真的需要顺序，还是使用状态机和幂等就能解决。",[18,16345,16346],{},"不要把所有消息都设置为同一个 Message Group，否则会把系统退化成串行消费。",[14,16348,16350],{"id":16349},"四定时与延时消息-delay","四、定时与延时消息 Delay",[473,16352,16354],{"id":16353},"_1-定时消息和延时消息有什么区别","1. 定时消息和延时消息有什么区别",[18,16356,16357],{},"业务表达方式不同，但在 RocketMQ 5.x 中本质相同：都是设置一个未来的投递时间戳。",[144,16359,16362],{"className":16360,"code":16361,"language":149,"meta":150},[147],"延时消息：10 分钟后投递\n定时消息：2026-07-26 18:00:00 投递\n",[22,16363,16361],{"__ignoreMap":150},[18,16365,16366],{},"延时消息也需要先计算出最终投递时间：",[144,16368,16370],{"className":197,"code":16369,"language":199,"meta":150,"style":150},"long deliveryTimestamp =\n        System.currentTimeMillis() + Duration.ofMinutes(10).toMillis();\n",[22,16371,16372,16383],{"__ignoreMap":150},[203,16373,16374,16377,16380],{"class":205,"line":206},[203,16375,16376],{"class":209},"long",[203,16378,16379],{"class":219}," deliveryTimestamp ",[203,16381,16382],{"class":209},"=\n",[203,16384,16385,16388,16391,16394,16396,16399,16402,16404,16406,16409,16412],{"class":205,"line":251},[203,16386,16387],{"class":219},"        System.",[203,16389,16390],{"class":215},"currentTimeMillis",[203,16392,16393],{"class":219},"() ",[203,16395,11953],{"class":209},[203,16397,16398],{"class":219}," Duration.",[203,16400,16401],{"class":215},"ofMinutes",[203,16403,220],{"class":219},[203,16405,2461],{"class":718},[203,16407,16408],{"class":219},").",[203,16410,16411],{"class":215},"toMillis",[203,16413,257],{"class":219},[473,16415,16417],{"id":16416},"_2-消息生命周期","2. 消息生命周期",[144,16419,16422],{"className":16420,"code":16421,"language":149,"meta":150},[147],"Producer 发送\n      ↓\nTiming：Broker 按时间暂存\n      ↓ 到达投递时间\nReady：转入普通存储并对 Consumer 可见\n      ↓\nConsumer 消费并确认\n",[22,16423,16421],{"__ignoreMap":150},[18,16425,16426],{},"消息到达 Broker 并不代表 Consumer 立即可见。只有达到投递时间，消息才进入可以消费的状态。",[473,16428,16430],{"id":16429},"_3-适用场景","3. 适用场景",[159,16432,16433,16436,16439,16442,16445,16448,16451],{},[162,16434,16435],{},"订单创建三十分钟后检查是否支付；",[162,16437,16438],{},"支付处理中，五分钟后查询渠道结果；",[162,16440,16441],{},"优惠券到期提醒；",[162,16443,16444],{},"用户预约通知；",[162,16446,16447],{},"延迟重试；",[162,16449,16450],{},"定时清理任务；",[162,16452,16453],{},"分布式定时调度。",[18,16455,16456],{},"例如订单超时关闭：",[144,16458,16461],{"className":16459,"code":16460,"language":149,"meta":150},[147],"创建订单\n   │\n   ├── 写入订单数据\n   └── 发送 30 分钟后投递的 Delay 消息\n                         ↓\n                 Consumer 查询订单状态\n                    ├── 未支付：关闭订单\n                    └── 已支付：忽略消息\n",[22,16462,16460],{"__ignoreMap":150},[18,16464,16465],{},"Consumer 收到延时消息后仍然必须重新检查业务状态。消息表达的是“现在应该检查”，不是“订单一定未支付”。",[473,16467,16469],{"id":16468},"_4-延时消息不是精确定时器","4. 延时消息不是精确定时器",[18,16471,16472,16473,16476],{},"设置的时间表示消息",[110,16474,16475],{},"不应早于该时间被消费","，但以下因素可能使实际处理晚于目标时间：",[159,16478,16479,16482,16485,16488,16491],{},[162,16480,16481],{},"Broker 当前负载较高；",[162,16483,16484],{},"大量消息设置了相同投递时间；",[162,16486,16487],{},"消费端发生积压；",[162,16489,16490],{},"Consumer 执行较慢或暂时不可用；",[162,16492,16493],{},"存储系统故障或重启。",[18,16495,16496],{},"因此，对秒级严格准点有强要求的业务不能只依赖消息到达时间，还要监控投递延迟并准备补偿扫描。",[473,16498,16500],{"id":16499},"_5-rocketmq-4x-与-5x-的区别","5. RocketMQ 4.x 与 5.x 的区别",[18,16502,16503],{},"RocketMQ 4.x 常见实现使用固定的延迟级别，例如：",[144,16505,16508],{"className":16506,"code":16507,"language":149,"meta":150},[147],"1 秒、5 秒、10 秒、30 秒、1 分钟……\n",[22,16509,16507],{"__ignoreMap":150},[18,16511,16512,16513,16516],{},"生产者通过 ",[22,16514,16515],{},"delayTimeLevel"," 选择预设级别。",[18,16518,16519],{},"RocketMQ 5.x 的正式 Delay 消息使用毫秒级 Unix 时间戳表示投递时间，不再局限于旧版固定延迟级别。面试时需要先说明讨论的是哪个版本，避免把 4.x 的十八个延迟级别直接当作 5.x 的唯一实现。",[14,16521,16523],{"id":16522},"五事务消息-transaction","五、事务消息 Transaction",[473,16525,16527],{"id":16526},"_1-事务消息解决什么问题","1. 事务消息解决什么问题",[18,16529,16530],{},"事务消息解决的是：",[105,16532,16533],{},[18,16534,16535],{},"本地事务与消息发送之间的最终一致性。",[18,16537,16538],{},"例如订单服务需要同时完成：",[2695,16540,16541,16544],{},[162,16542,16543],{},"在本地数据库创建订单；",[162,16545,16546],{},"向 RocketMQ 发送订单创建消息。",[18,16548,16549],{},"如果先提交数据库再发送消息，数据库成功后消息可能发送失败；如果先发送消息再写数据库，消息可能已被消费，但数据库事务最终失败。",[144,16551,16554],{"className":16552,"code":16553,"language":149,"meta":150},[147],"数据库事务成功，消息失败  → 下游永远不知道订单已创建\n消息发送成功，数据库失败  → 下游处理了一笔不存在的订单\n",[22,16555,16553],{"__ignoreMap":150},[473,16557,16559],{"id":16558},"_2-核心流程","2. 核心流程",[144,16561,16564],{"className":16562,"code":16563,"language":149,"meta":150},[147],"Producer 发送 Half Message\n           ↓\nBroker 保存消息，但暂不向 Consumer 投递\n           ↓\nProducer 执行本地事务\n       ┌───┴────────┐\n       │            │\n    Commit       Rollback\n       │            │\n消息可见并投递    消息回滚\n",[22,16565,16563],{"__ignoreMap":150},[18,16567,16568],{},"如果 Broker 长时间没有收到明确结果，会回查 Producer：",[144,16570,16573],{"className":16571,"code":16572,"language":149,"meta":150},[147],"Broker 发起事务回查\n          ↓\nProducer 查询本地事务记录\n     ┌────┼─────┐\n  Commit Unknown Rollback\n",[22,16574,16572],{"__ignoreMap":150},[18,16576,16577],{},"Producer 的事务回查不能只依赖内存变量，需要根据订单表、事务日志或本地 Outbox 等持久化数据判断。",[473,16579,16430],{"id":16580},"_3-适用场景-1",[159,16582,16583,16586,16589,16592,16595],{},[162,16584,16585],{},"订单创建成功后通知库存、积分系统；",[162,16587,16588],{},"扣款成功后发送账务事件；",[162,16590,16591],{},"账户入账成功后通知下游；",[162,16593,16594],{},"数据库状态变化后更新搜索索引；",[162,16596,16597],{},"本地事务成功后驱动跨服务异步流程。",[473,16599,16601],{"id":16600},"_4-事务消息不保证什么","4. 事务消息不保证什么",[18,16603,16604],{},"事务消息保证的是本地事务与消息是否投递之间的最终一致性，不等于整个分布式业务已经成功。",[18,16606,686],{},[144,16608,16611],{"className":16609,"code":16610,"language":149,"meta":150},[147],"订单本地事务成功\n       ↓\n事务消息 Commit\n       ↓\n积分服务消费失败\n",[22,16612,16610],{"__ignoreMap":150},[18,16614,16615],{},"RocketMQ 可以重试投递，但积分服务仍需实现：",[159,16617,16618,16621,16624,16627,16630],{},[162,16619,16620],{},"消费幂等；",[162,16622,16623],{},"失败重试；",[162,16625,16626],{},"死信处理；",[162,16628,16629],{},"对账与补偿；",[162,16631,16632],{},"业务状态机。",[18,16634,16635],{},"事务消息也不是跨多个数据库的强一致分布式事务。它更适合能够接受异步执行和最终一致性的业务。",[14,16637,16639],{"id":16638},"六四种消息类型如何选择","六、四种消息类型如何选择",[18,16641,16642],{},"可以按照下面的顺序判断：",[144,16644,16647],{"className":16645,"code":16646,"language":149,"meta":150},[147],"本地事务是否必须与消息发送保持最终一致？\n    ├── 是 → Transaction\n    └── 否\n         ↓\n消息是否需要到未来某个时间才可消费？\n    ├── 是 → Delay\n    └── 否\n         ↓\n同一业务实体的事件是否必须按顺序处理？\n    ├── 是 → FIFO\n    └── 否 → Normal\n",[22,16648,16646],{"__ignoreMap":150},[18,16650,16651],{},"对应关系：",[299,16653,16654,16667],{},[302,16655,16656],{},[305,16657,16658,16661,16664],{},[308,16659,16660],{},"业务要求",[308,16662,16663],{},"推荐类型",[308,16665,16666],{},"关键设计点",[315,16668,16669,16680,16690,16701],{},[305,16670,16671,16674,16677],{},[320,16672,16673],{},"只需要可靠异步传输",[320,16675,16676],{},"Normal",[320,16678,16679],{},"重试、幂等、监控",[305,16681,16682,16685,16687],{},[320,16683,16684],{},"同一订单内有先后关系",[320,16686,16057],{},[320,16688,16689],{},"Message Group、串行处理",[305,16691,16692,16695,16698],{},[320,16693,16694],{},"未来某个时间触发",[320,16696,16697],{},"Delay",[320,16699,16700],{},"投递时间、状态复查、延迟监控",[305,16702,16703,16706,16709],{},[320,16704,16705],{},"本地事务成功后才能投递",[320,16707,16708],{},"Transaction",[320,16710,16711],{},"Half Message、事务回查、持久化事务状态",[18,16713,16714],{},"不要因为高级消息类型“功能更多”就默认使用。每种特殊语义都会增加约束、运维成本或吞吐损耗；没有特殊要求时，普通消息通常是更简单的选择。",[14,16716,16718],{"id":16717},"七这些算不算消息类型","七、这些算不算消息类型",[473,16720,16722],{"id":16721},"_1-批量消息","1. 批量消息",[18,16724,16725],{},"批量消息不是独立 MessageType，而是一种发送或消费方式。",[144,16727,16730],{"className":16728,"code":16729,"language":149,"meta":150},[147],"单条发送：一次请求发送一条消息\n批量发送：一次请求携带多条消息\n",[22,16731,16729],{"__ignoreMap":150},[18,16733,16734],{},"批量能够减少网络往返和请求开销，适合日志采集、数据同步等吞吐优先的场景，但需要注意：",[159,16736,16737,16740,16743,16746],{},[162,16738,16739],{},"控制整个批次的大小；",[162,16741,16742],{},"批次失败后的重试粒度；",[162,16744,16745],{},"顺序消息不应随意并行批处理；",[162,16747,16748],{},"批量消费部分成功时的幂等和重试问题。",[473,16750,16752],{"id":16751},"_2-重试消息","2. 重试消息",[18,16754,16755],{},"重试消息是普通消息、FIFO 消息等消费失败后，RocketMQ 根据消费重试策略重新投递形成的系统处理过程，不是业务创建 Topic 时选择的正式 MessageType。",[18,16757,16758],{},"对于 PushConsumer，一次消费及失败重试可以简化为：",[144,16760,16763],{"className":16761,"code":16762,"language":149,"meta":150},[147],"Ready\n  ↓ Consumer 获取消息\nInflight\n  ├─ 消费成功并返回结果 → Commit\n  └─ 消费失败或等待结果超时 → WaitingRetry\n                                  ├─ 到达重试时间 → Ready\n                                  └─ 超过最大重试次数 → DLQ\n",[22,16764,16762],{"__ignoreMap":150},[18,16766,16767,16770],{},[22,16768,16769],{},"Inflight"," 表示消息已经被 Consumer 获取，Consumer 正在执行业务逻辑，但消费结果还没有返回给 Broker。它可以理解为“正在处理中、等待确认”，不是一种新的消息类型。",[18,16772,16773,16776,16777,16779,16780,16783],{},[22,16774,16775],{},"WaitingRetry"," 是 PushConsumer 重试状态机中的状态。消息消费失败或 Broker 等待消费结果超时后，如果尚未达到最大重试次数，消息会先进入 ",[22,16778,16775],{},"；重试间隔结束后重新回到 ",[22,16781,16782],{},"Ready","，等待下一次投递。",[18,16785,16786],{},"Consumer 必须按消息可能重复投递来设计幂等。",[473,16788,16790],{"id":16789},"_3-死信消息","3. 死信消息",[18,16792,16793],{},"消息超过最大消费重试次数后，可能进入死信队列。死信消息代表消息进入异常隔离状态，也不是四种正式业务 MessageType 之一。",[18,16795,16796],{},"死信队列需要配套：",[159,16798,16799,16802,16805,16808,16811],{},[162,16800,16801],{},"告警；",[162,16803,16804],{},"失败原因记录；",[162,16806,16807],{},"人工检查；",[162,16809,16810],{},"修复后的重投；",[162,16812,16813],{},"对账和业务补偿。",[473,16815,16817],{"id":16816},"_4-request-reply-消息","4. Request-Reply 消息",[18,16819,16820],{},"Request-Reply 是一种通信模式：",[144,16822,16825],{"className":16823,"code":16824,"language":149,"meta":150},[147],"请求方发送消息\n      ↓\n处理方消费并返回响应\n      ↓\n请求方等待响应\n",[22,16826,16824],{"__ignoreMap":150},[18,16828,16829,16830,53,16832,53,16834,53,16836,16838],{},"它描述生产者和消费者如何交互，不属于 ",[22,16831,16044],{},[22,16833,16057],{},[22,16835,16070],{},[22,16837,16083],{}," 之外的第五种正式 MessageType。",[473,16840,16842],{"id":16841},"_5-同步异步和单向消息","5. 同步、异步和单向消息",[18,16844,16845],{},"同步发送、异步发送和单向发送描述的是 Producer 调用发送接口的方式，也不是消息类型。",[144,16847,16850],{"className":16848,"code":16849,"language":149,"meta":150},[147],"同步发送：等待 Broker 返回结果\n异步发送：通过回调接收结果\n单向发送：发送后不等待结果\n",[22,16851,16849],{"__ignoreMap":150},[18,16853,16854],{},"不要把“发送方式”和“消息传输语义”混为一谈。",[14,16856,16858],{"id":16857},"八rocketmq-5x-为什么要求-topic-类型一致","八、RocketMQ 5.x 为什么要求 Topic 类型一致",[18,16860,16861],{},"RocketMQ 5.x 支持在 Topic 上声明消息类型：",[144,16863,16866],{"className":16864,"code":16865,"language":149,"meta":150},[147],".\u002Fbin\u002Fmqadmin updateTopic \\\n  -n 127.0.0.1:9876 \\\n  -t order-events \\\n  -c DefaultCluster \\\n  -a \"+message.type=FIFO\"\n",[22,16867,16865],{"__ignoreMap":150},[18,16869,16870],{},"其中：",[159,16872,16873,16879,16885,16891],{},[162,16874,16875,16878],{},[22,16876,16877],{},"-n"," 指定 NameServer 地址；",[162,16880,16881,16884],{},[22,16882,16883],{},"-t"," 指定 Topic 名称；",[162,16886,16887,16890],{},[22,16888,16889],{},"-c"," 指定目标集群；",[162,16892,16893,16896,16897,53,16899,53,16901,6265,16903,33],{},[22,16894,16895],{},"message.type"," 可设置为 ",[22,16898,16044],{},[22,16900,16057],{},[22,16902,16070],{},[22,16904,16083],{},[18,16906,16907],{},"上面创建的是 FIFO Topic。创建其他类型时，只需要替换最后一个参数的类型值。",[18,16909,16910],{},"启用类型校验后，一个 Topic 只允许发送与其类型一致的消息。例如：",[159,16912,16913,16921,16927],{},[162,16914,16915,16917,16918,16920],{},[22,16916,16057],{}," 消息不能发送到 ",[22,16919,16044],{}," Topic；",[162,16922,16923,16917,16925,16920],{},[22,16924,16083],{},[22,16926,16070],{},[162,16928,16929],{},"同一个 Topic 不应同时承载普通消息和顺序消息。",[18,16931,16932],{},"这样做的价值是：",[159,16934,16935,16938,16941,16944],{},[162,16936,16937],{},"让 Topic 的行为语义清晰；",[162,16939,16940],{},"避免客户端误用高级消息能力；",[162,16942,16943],{},"便于 Broker 按消息类型管理；",[162,16945,16946],{},"降低运维和故障排查复杂度。",[18,16948,16949],{},"为了兼容旧客户端，5.x 的强制类型校验能力需要结合服务端配置判断。面试中可以说“5.x 支持并推荐一个 Topic 对应一种消息类型”，不要绝对化为所有兼容部署都默认强制拒绝。",[14,16951,16953],{"id":16952},"九常见追问","九、常见追问",[473,16955,16957],{"id":16956},"_1-普通消息一定无序吗","1. 普通消息一定无序吗",[18,16959,16960],{},"普通消息在某个物理队列中自然存在存储先后，但业务没有建立端到端 FIFO 约束。多队列发送、并发消费、失败重试都可能使业务完成顺序变化，因此不能依赖普通消息保证业务顺序。",[473,16962,16964],{"id":16963},"_2-顺序消息能否保证整个-topic-全局有序","2. 顺序消息能否保证整个 Topic 全局有序",[18,16966,16967],{},"实际通常保证同一 Message Group 内有序。理论上把所有消息放入一个组可以接近全局串行，但会形成热点并严重限制吞吐，不建议这样设计。",[473,16969,16971],{"id":16970},"_3-延时消息到点后一定立即执行吗","3. 延时消息到点后一定立即执行吗",[18,16973,16974],{},"不一定。到点表示消息可以进入可消费状态，Broker 调度延迟、消息积压和 Consumer 处理能力都会影响最终业务执行时间。",[473,16976,16978],{"id":16977},"_4-事务消息消费成功是否与本地事务强一致","4. 事务消息消费成功是否与本地事务强一致",[18,16980,16981],{},"不是。事务消息只保证本地事务结果与消息是否投递之间的最终一致性。Consumer 的业务处理仍然是独立的异步过程，需要重试、幂等和补偿。",[473,16983,16985],{"id":16984},"_5-fifo-和-transaction-能否同时使用","5. FIFO 和 Transaction 能否同时使用",[18,16987,16988],{},"从 RocketMQ 5.x 的 Topic 类型模型看，一个 Topic 只对应一种正式消息类型，不能把同一条消息同时声明为 FIFO 和 Transaction。业务同时需要“事务一致性”和“顺序处理”时，需要重新审视领域流程，常见做法是用事务消息可靠发布业务事件，再由下游状态机、版本号、业务序号或独立 FIFO 流程处理顺序要求。",[14,16990,16992],{"id":16991},"十容易答错的地方","十、容易答错的地方",[473,16994,16996],{"id":16995},"错误一rocketmq-有五种消息包含批量消息","错误一：RocketMQ 有五种消息，包含批量消息",[18,16998,16999],{},"批量是发送方式，不是 RocketMQ 5.x 的正式 MessageType。正式类型是 Normal、FIFO、Delay、Transaction。",[473,17001,17003],{"id":17002},"错误二顺序消息保证整个-topic-全局有序","错误二：顺序消息保证整个 Topic 全局有序",[18,17005,17006],{},"通常保证同一 Message Group 内的局部顺序，不同组之间允许并发。",[473,17008,17010],{"id":17009},"错误三延时消息到了时间就保证业务已经执行","错误三：延时消息到了时间就保证业务已经执行",[18,17012,17013],{},"到达投递时间只表示消息对 Consumer 可见，实际业务完成还受积压、消费能力和重试影响。",[473,17015,17017],{"id":17016},"错误四事务消息保证上下游所有业务一起提交","错误四：事务消息保证上下游所有业务一起提交",[18,17019,17020],{},"事务消息保证本地事务与消息投递最终一致，下游消费结果仍然需要独立保障。",[473,17022,17024],{"id":17023},"错误五普通消息可靠性低","错误五：普通消息可靠性低",[18,17026,17027],{},"普通消息只是没有高级传输语义，不代表不持久化、不重试或不可靠。",[14,17029,5803],{"id":5803},[159,17031,17032,17039,17046,17053,17060,17067,17074],{},[162,17033,17034],{},[119,17035,17038],{"href":17036,"rel":17037},"https:\u002F\u002Frocketmq.apache.org\u002Fdocs\u002FdomainModel\u002F02topic\u002F",[2080],"Apache RocketMQ 5.0：Topic 与 MessageType",[162,17040,17041],{},[119,17042,17045],{"href":17043,"rel":17044},"https:\u002F\u002Frocketmq.apache.org\u002Fdocs\u002FfeatureBehavior\u002F01normalmessage\u002F",[2080],"Apache RocketMQ 5.0：普通消息",[162,17047,17048],{},[119,17049,17052],{"href":17050,"rel":17051},"https:\u002F\u002Frocketmq.apache.org\u002Fdocs\u002FfeatureBehavior\u002F03fifomessage\u002F",[2080],"Apache RocketMQ 5.0：顺序消息",[162,17054,17055],{},[119,17056,17059],{"href":17057,"rel":17058},"https:\u002F\u002Frocketmq.apache.org\u002Fdocs\u002FfeatureBehavior\u002F02delaymessage\u002F",[2080],"Apache RocketMQ 5.0：定时与延时消息",[162,17061,17062],{},[119,17063,17066],{"href":17064,"rel":17065},"https:\u002F\u002Frocketmq.apache.org\u002Fdocs\u002FfeatureBehavior\u002F04transactionmessage\u002F",[2080],"Apache RocketMQ 5.0：事务消息",[162,17068,17069],{},[119,17070,17073],{"href":17071,"rel":17072},"https:\u002F\u002Frocketmq.apache.org\u002Fdocs\u002FfeatureBehavior\u002F10consumerretrypolicy\u002F",[2080],"Apache RocketMQ 5.0：消费重试",[162,17075,17076],{},[119,17077,17080],{"href":17078,"rel":17079},"https:\u002F\u002Frocketmq.apache.org\u002Fdocs\u002F4.x\u002Fproducer\u002F04message3\u002F",[2080],"Apache RocketMQ 4.x：延时消息级别",[2111,17082,17083],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":150,"searchDepth":251,"depth":251,"links":17085},[17086,17087,17088,17089,17090,17095,17102,17109,17115,17116,17123,17124,17131,17138],{"id":16,"depth":251,"text":16},{"id":115,"depth":251,"text":115},{"id":132,"depth":251,"text":132},{"id":16017,"depth":251,"text":16018},{"id":16115,"depth":251,"text":16116,"children":17091},[17092,17093,17094],{"id":16119,"depth":260,"text":16120},{"id":16143,"depth":260,"text":16144},{"id":16185,"depth":260,"text":16186},{"id":16206,"depth":251,"text":16207,"children":17096},[17097,17098,17099,17100,17101],{"id":16210,"depth":260,"text":16211},{"id":16245,"depth":260,"text":16246},{"id":16264,"depth":260,"text":16265},{"id":16295,"depth":260,"text":16296},{"id":16319,"depth":260,"text":16320},{"id":16349,"depth":251,"text":16350,"children":17103},[17104,17105,17106,17107,17108],{"id":16353,"depth":260,"text":16354},{"id":16416,"depth":260,"text":16417},{"id":16429,"depth":260,"text":16430},{"id":16468,"depth":260,"text":16469},{"id":16499,"depth":260,"text":16500},{"id":16522,"depth":251,"text":16523,"children":17110},[17111,17112,17113,17114],{"id":16526,"depth":260,"text":16527},{"id":16558,"depth":260,"text":16559},{"id":16580,"depth":260,"text":16430},{"id":16600,"depth":260,"text":16601},{"id":16638,"depth":251,"text":16639},{"id":16717,"depth":251,"text":16718,"children":17117},[17118,17119,17120,17121,17122],{"id":16721,"depth":260,"text":16722},{"id":16751,"depth":260,"text":16752},{"id":16789,"depth":260,"text":16790},{"id":16816,"depth":260,"text":16817},{"id":16841,"depth":260,"text":16842},{"id":16857,"depth":251,"text":16858},{"id":16952,"depth":251,"text":16953,"children":17125},[17126,17127,17128,17129,17130],{"id":16956,"depth":260,"text":16957},{"id":16963,"depth":260,"text":16964},{"id":16970,"depth":260,"text":16971},{"id":16977,"depth":260,"text":16978},{"id":16984,"depth":260,"text":16985},{"id":16991,"depth":251,"text":16992,"children":17132},[17133,17134,17135,17136,17137],{"id":16995,"depth":260,"text":16996},{"id":17002,"depth":260,"text":17003},{"id":17009,"depth":260,"text":17010},{"id":17016,"depth":260,"text":17017},{"id":17023,"depth":260,"text":17024},{"id":5803,"depth":251,"text":5803},"wiki:java:rocketmq-message-types-and-use-cases","系统介绍 RocketMQ 5.x 的普通消息、顺序消息、定时与延时消息、事务消息，说明各自语义、适用场景、使用限制，并澄清批量消息、重试消息和死信消息是否属于消息类型。",{},78,"\u002Fjava\u002Frocketmq-message-types-and-use-cases",{"title":15953,"description":17140},"java\u002Frocketmq-message-types-and-use-cases","2026-07-26","0Dk0ULElt6DDqSea9SIrQh6YmMktZE9yv-Ac_3lFtCs",{"id":17149,"title":17150,"body":17151,"commentId":19198,"description":19199,"difficulty":4584,"draft":2148,"extension":2149,"meta":19200,"navigation":756,"order":19201,"path":19202,"section":2153,"seo":19203,"stem":19204,"updated":19205,"__hash__":19206},"java\u002Fjava\u002Fjava-thread-pool-core-parameters-and-practice.md","Java 线程池核心参数有哪些？日常应该怎么使用？",{"type":7,"value":17152,"toc":19154},[17153,17156,17158,17191,17194,17197,17199,17206,17208,17217,17219,17223,17226,17240,17243,17249,17253,17256,17323,17327,17332,17335,17350,17353,17371,17376,17380,17385,17388,17394,17399,17403,17409,17417,17430,17433,17436,17444,17448,17455,17472,17475,17479,17485,17488,17561,17564,17581,17584,17588,17593,17607,17610,17616,17619,17623,17626,17629,17700,17703,17707,17710,17725,17730,17736,17739,17742,17746,17749,17755,17758,17764,17770,17779,17783,17787,17790,17793,17799,17802,17806,17809,17815,17818,17824,17827,17844,17847,17851,17854,17868,17871,17877,17880,17884,17887,18565,18568,18590,18593,18597,18603,18607,18616,18622,18626,18632,18638,18649,18652,18658,18664,18675,18681,18685,18687,18699,18712,18715,18741,18771,18783,18787,18790,18792,18798,18801,18818,18821,18825,18828,18933,18936,18956,18959,18963,18972,18978,18981,18987,18990,19000,19008,19014,19019,19025,19029,19033,19036,19040,19043,19047,19052,19056,19059,19063,19074,19078,19081,19085,19126,19128,19151],[10,17154,17150],{"id":17155},"java-线程池核心参数有哪些日常应该怎么使用",[14,17157,16],{"id":16},[18,17159,17160,17163,17164,17167,17168,17171,17172,17175,17176,17179,17180,17183,17184,17187,17188,33],{},[22,17161,17162],{},"ThreadPoolExecutor"," 有七个核心参数：核心线程数 ",[22,17165,17166],{},"corePoolSize","、最大线程数 ",[22,17169,17170],{},"maximumPoolSize","、非核心线程空闲存活时间 ",[22,17173,17174],{},"keepAliveTime","、时间单位 ",[22,17177,17178],{},"unit","、任务队列 ",[22,17181,17182],{},"workQueue","、线程工厂 ",[22,17185,17186],{},"threadFactory"," 和拒绝策略 ",[22,17189,17190],{},"handler",[18,17192,17193],{},"提交任务后，线程池先判断当前工作线程数是否小于核心线程数，是则创建线程执行；否则尝试将任务放入队列。队列已满时，如果线程数还没有达到最大线程数，就继续创建非核心线程；线程数达到最大值后仍无法接收任务，才执行拒绝策略。因此，最大线程数是否生效与队列是否有界密切相关。",[18,17195,17196],{},"日常使用时，应根据任务是 CPU 密集型还是 I\u002FO 密集型划分独立线程池，明确核心线程数、最大线程数和有界队列，设置有业务含义的线程名，并根据业务选择可观测、可补偿的拒绝策略。同时监控活跃线程数、队列长度、拒绝次数、排队耗时和执行耗时，应用停止时进行优雅关闭。线程数和队列容量不能只靠固定公式决定，还要结合机器 CPU、任务等待与计算比例、下游容量及允许的业务延迟进行压测和调整。",[18,17198,103],{},[105,17200,17201],{},[18,17202,17203],{},[110,17204,17205],{},"线程池用有限的线程和队列承接任务；日常配置的核心不是把参数设大，而是明确资源上限、过载行为和监控补偿机制。",[14,17207,115],{"id":115},[18,17209,17210],{},[119,17211,17214],{"href":17212,"target":122,"rel":17213},"\u002Fimages\u002Fwiki\u002Fjava\u002Fjava-thread-pool-core-parameters-and-practice.svg",[124,125],[127,17215],{"src":17212,"alt":17216},"Java 线程池七个核心参数、任务提交流程和日常使用原则",[14,17218,132],{"id":132},[14,17220,17222],{"id":17221},"一为什么使用线程池","一、为什么使用线程池",[18,17224,17225],{},"如果每个任务都创建一个新线程，会带来以下问题：",[159,17227,17228,17231,17234,17237],{},[162,17229,17230],{},"创建和销毁线程需要消耗时间与内存；",[162,17232,17233],{},"线程数量缺少上限，突发流量可能耗尽系统资源；",[162,17235,17236],{},"大量线程会增加上下文切换和调度开销；",[162,17238,17239],{},"无法统一管理任务排队、拒绝、监控和关闭。",[18,17241,17242],{},"线程池的核心价值不是单纯复用线程，而是对并发资源进行治理：",[144,17244,17247],{"className":17245,"code":17246,"language":149,"meta":150},[147],"控制线程数量\n+ 缓冲暂时处理不了的任务\n+ 定义系统过载时的行为\n+ 统一监控和生命周期管理\n",[22,17248,17246],{"__ignoreMap":150},[14,17250,17252],{"id":17251},"二threadpoolexecutor-的七个核心参数","二、ThreadPoolExecutor 的七个核心参数",[18,17254,17255],{},"常用构造方法如下：",[144,17257,17259],{"className":197,"code":17258,"language":199,"meta":150,"style":150},"public ThreadPoolExecutor(\n        int corePoolSize,\n        int maximumPoolSize,\n        long keepAliveTime,\n        TimeUnit unit,\n        BlockingQueue\u003CRunnable> workQueue,\n        ThreadFactory threadFactory,\n        RejectedExecutionHandler handler)\n",[22,17260,17261,17270,17278,17285,17293,17298,17313,17318],{"__ignoreMap":150},[203,17262,17263,17265,17268],{"class":205,"line":206},[203,17264,441],{"class":209},[203,17266,17267],{"class":215}," ThreadPoolExecutor",[203,17269,1496],{"class":219},[203,17271,17272,17275],{"class":205,"line":251},[203,17273,17274],{"class":209},"        int",[203,17276,17277],{"class":219}," corePoolSize,\n",[203,17279,17280,17282],{"class":205,"line":260},[203,17281,17274],{"class":209},[203,17283,17284],{"class":219}," maximumPoolSize,\n",[203,17286,17287,17290],{"class":205,"line":278},[203,17288,17289],{"class":209},"        long",[203,17291,17292],{"class":219}," keepAliveTime,\n",[203,17294,17295],{"class":205,"line":285},[203,17296,17297],{"class":219},"        TimeUnit unit,\n",[203,17299,17300,17303,17305,17308,17310],{"class":205,"line":541},[203,17301,17302],{"class":219},"        BlockingQueue",[203,17304,3306],{"class":209},[203,17306,17307],{"class":219},"Runnable",[203,17309,9371],{"class":209},[203,17311,17312],{"class":219}," workQueue,\n",[203,17314,17315],{"class":205,"line":549},[203,17316,17317],{"class":219},"        ThreadFactory threadFactory,\n",[203,17319,17320],{"class":205,"line":787},[203,17321,17322],{"class":219},"        RejectedExecutionHandler handler)\n",[473,17324,17326],{"id":17325},"_1-corepoolsize核心线程数","1. corePoolSize：核心线程数",[18,17328,17329,17331],{},[22,17330,17166],{}," 表示线程池希望长期保留的基本工作线程数量。",[18,17333,17334],{},"默认情况下，核心线程也不是在线程池创建时立即启动，而是在任务到来后按需创建。需要提前完成线程创建时，可以调用：",[144,17336,17338],{"className":197,"code":17337,"language":199,"meta":150,"style":150},"threadPool.prestartAllCoreThreads();\n",[22,17339,17340],{"__ignoreMap":150},[203,17341,17342,17345,17348],{"class":205,"line":206},[203,17343,17344],{"class":219},"threadPool.",[203,17346,17347],{"class":215},"prestartAllCoreThreads",[203,17349,257],{"class":219},[18,17351,17352],{},"核心线程默认不会因为空闲而退出。调用以下方法后，核心线程也可以受空闲超时控制：",[144,17354,17356],{"className":197,"code":17355,"language":199,"meta":150,"style":150},"threadPool.allowCoreThreadTimeOut(true);\n",[22,17357,17358],{"__ignoreMap":150},[203,17359,17360,17362,17365,17367,17369],{"class":205,"line":206},[203,17361,17344],{"class":219},[203,17363,17364],{"class":215},"allowCoreThreadTimeOut",[203,17366,220],{"class":219},[203,17368,5166],{"class":718},[203,17370,227],{"class":219},[18,17372,13662,17373,17375],{},[22,17374,17174],{}," 必须大于零。",[473,17377,17379],{"id":17378},"_2-maximumpoolsize最大线程数","2. maximumPoolSize：最大线程数",[18,17381,17382,17384],{},[22,17383,17170],{}," 是线程池允许存在的最大工作线程数量，必须大于或等于核心线程数。",[18,17386,17387],{},"它并不是任务一多就立即生效。只有满足以下条件时，线程池才会创建超过核心数量的线程：",[144,17389,17392],{"className":17390,"code":17391,"language":149,"meta":150},[147],"核心线程都在工作\n+ workQueue 已满\n+ 当前线程数小于 maximumPoolSize\n",[22,17393,17391],{"__ignoreMap":150},[18,17395,17396,17397,33],{},"如果使用不会被填满的无界队列，任务会持续进入队列，线程池通常不会扩展到 ",[22,17398,17170],{},[473,17400,17402],{"id":17401},"_3-keepalivetime空闲线程存活时间","3. keepAliveTime：空闲线程存活时间",[18,17404,17405,17406,17408],{},"当工作线程数量超过 ",[22,17407,17166],{}," 时，多出来的线程如果在指定时间内没有获取到任务，就会被回收。",[18,17410,17411,17413,17414,17416],{},[22,17412,17174],{}," 决定空闲等待时长，",[22,17415,17178],{}," 决定它的时间单位：",[144,17418,17420],{"className":197,"code":17419,"language":199,"meta":150,"style":150},"60L, TimeUnit.SECONDS\n",[22,17421,17422],{"__ignoreMap":150},[203,17423,17424,17427],{"class":205,"line":206},[203,17425,17426],{"class":718},"60L",[203,17428,17429],{"class":219},", TimeUnit.SECONDS\n",[18,17431,17432],{},"它们共同表示空闲线程最多等待 60 秒。",[18,17434,17435],{},"该参数是在以下两者之间取舍：",[159,17437,17438,17441],{},[162,17439,17440],{},"设置较短：流量下降后能更快释放线程资源，但下一次突发流量需要重新创建线程；",[162,17442,17443],{},"设置较长：可以减少线程反复创建，但空闲线程会占用更多内存。",[473,17445,17447],{"id":17446},"_4-unit时间单位","4. unit：时间单位",[18,17449,17450,2014,17452,17454],{},[22,17451,17178],{},[22,17453,17174],{}," 的单位，常用值包括：",[159,17456,17457,17462,17467],{},[162,17458,17459],{},[22,17460,17461],{},"TimeUnit.MILLISECONDS",[162,17463,17464],{},[22,17465,17466],{},"TimeUnit.SECONDS",[162,17468,17469],{},[22,17470,17471],{},"TimeUnit.MINUTES",[18,17473,17474],{},"把时间值和单位拆开传入，可以避免代码中出现含义不清的裸数字。",[473,17476,17478],{"id":17477},"_5-workqueue任务队列","5. workQueue：任务队列",[18,17480,17481,17482,17484],{},"当核心线程都在工作时，新任务会先尝试进入 ",[22,17483,17182],{}," 等待，而不是立即创建非核心线程。",[18,17486,17487],{},"常见队列如下：",[299,17489,17490,17503],{},[302,17491,17492],{},[305,17493,17494,17497,17500],{},[308,17495,17496],{},"队列",[308,17498,17499],{},"特点",[308,17501,17502],{},"适用场景与注意事项",[315,17504,17505,17518,17535,17548],{},[305,17506,17507,17512,17515],{},[320,17508,17509],{},[22,17510,17511],{},"ArrayBlockingQueue",[320,17513,17514],{},"基于数组的有界队列，容量固定",[320,17516,17517],{},"资源边界明确，适合大多数需要控制积压的服务端任务",[305,17519,17520,17525,17528],{},[320,17521,17522],{},[22,17523,17524],{},"LinkedBlockingQueue",[320,17526,17527],{},"基于链表，可指定容量",[320,17529,17530,17531,17534],{},"不传容量时上限为 ",[22,17532,17533],{},"Integer.MAX_VALUE","，容易隐藏长期积压",[305,17536,17537,17542,17545],{},[320,17538,17539],{},[22,17540,17541],{},"SynchronousQueue",[320,17543,17544],{},"不保存任务，每次提交都直接移交给工作线程",[320,17546,17547],{},"适合希望快速扩展线程、任务很短且能够严格控制最大线程数的场景",[305,17549,17550,17555,17558],{},[320,17551,17552],{},[22,17553,17554],{},"PriorityBlockingQueue",[320,17556,17557],{},"按优先级取任务，通常是无界队列",[320,17559,17560],{},"任务需要可比较；要防止低优先级任务长期得不到执行",[18,17562,17563],{},"队列容量并非越大越好。大队列虽然能暂时降低拒绝次数，却可能带来：",[159,17565,17566,17569,17572,17575,17578],{},[162,17567,17568],{},"任务排队时间不断增长；",[162,17570,17571],{},"内存占用上升；",[162,17573,17574],{},"故障被延迟暴露；",[162,17576,17577],{},"应用关闭时遗留大量未完成任务；",[162,17579,17580],{},"请求已经失去业务时效后仍在执行。",[18,17582,17583],{},"有界队列能够让系统在达到承载上限时尽早进入明确的过载处理流程。",[473,17585,17587],{"id":17586},"_6-threadfactory线程工厂","6. threadFactory：线程工厂",[18,17589,17590,17592],{},[22,17591,17186],{}," 负责创建工作线程，可以统一设置：",[159,17594,17595,17598,17601,17604],{},[162,17596,17597],{},"线程名称；",[162,17599,17600],{},"是否为守护线程；",[162,17602,17603],{},"线程优先级；",[162,17605,17606],{},"未捕获异常处理器。",[18,17608,17609],{},"线程名应体现业务用途，例如：",[144,17611,17614],{"className":17612,"code":17613,"language":149,"meta":150},[147],"order-notify-1\norder-notify-2\n",[22,17615,17613],{"__ignoreMap":150},[18,17617,17618],{},"出现高 CPU、死锁或任务堆积时，有业务含义的线程名能显著降低线程栈排查成本。",[473,17620,17622],{"id":17621},"_7-handler拒绝策略","7. handler：拒绝策略",[18,17624,17625],{},"当线程池没有运行，或者同时满足“线程数达到最大值、任务队列已满”时，新任务会被拒绝。",[18,17627,17628],{},"JDK 提供四种常用策略：",[299,17630,17631,17644],{},[302,17632,17633],{},[305,17634,17635,17638,17641],{},[308,17636,17637],{},"策略",[308,17639,17640],{},"行为",[308,17642,17643],{},"适用提醒",[315,17645,17646,17661,17674,17687],{},[305,17647,17648,17653,17658],{},[320,17649,17650],{},[22,17651,17652],{},"AbortPolicy",[320,17654,17655,17656],{},"抛出 ",[22,17657,910],{},[320,17659,17660],{},"默认策略，能够明确暴露过载，调用方需要处理异常",[305,17662,17663,17668,17671],{},[320,17664,17665],{},[22,17666,17667],{},"CallerRunsPolicy",[320,17669,17670],{},"由提交任务的线程直接执行",[320,17672,17673],{},"可以减慢生产者形成一定反压，但会增加调用线程延迟",[305,17675,17676,17681,17684],{},[320,17677,17678],{},[22,17679,17680],{},"DiscardPolicy",[320,17682,17683],{},"静默丢弃新任务",[320,17685,17686],{},"只适合明确允许丢弃且有独立监控的低价值任务",[305,17688,17689,17694,17697],{},[320,17690,17691],{},[22,17692,17693],{},"DiscardOldestPolicy",[320,17695,17696],{},"丢弃队列头任务，再尝试提交新任务",[320,17698,17699],{},"可能丢掉等待最久或优先级最高的任务，使用前必须确认业务语义",[18,17701,17702],{},"拒绝策略没有适用于所有业务的统一答案。核心交易任务通常不能静默丢弃，可以抛出异常后进入降级、补偿或持久化重试；允许采样的日志、指标任务才可能接受丢弃。",[14,17704,17706],{"id":17705},"三任务提交后的完整流程","三、任务提交后的完整流程",[18,17708,17709],{},"调用：",[144,17711,17713],{"className":197,"code":17712,"language":199,"meta":150,"style":150},"threadPool.execute(task);\n",[22,17714,17715],{"__ignoreMap":150},[203,17716,17717,17719,17722],{"class":205,"line":206},[203,17718,17344],{"class":219},[203,17720,17721],{"class":215},"execute",[203,17723,17724],{"class":219},"(task);\n",[18,17726,17727,17729],{},[22,17728,17162],{}," 的处理流程可以概括为：",[144,17731,17734],{"className":17732,"code":17733,"language":149,"meta":150},[147],"1. 当前工作线程数小于 corePoolSize\n   → 创建核心工作线程执行任务\n\n2. 当前工作线程数已达到 corePoolSize\n   → 尝试将任务加入 workQueue\n\n3. workQueue 已满，且工作线程数小于 maximumPoolSize\n   → 创建非核心工作线程执行任务\n\n4. workQueue 已满，且工作线程数已达到 maximumPoolSize\n   → 执行拒绝策略\n",[22,17735,17733],{"__ignoreMap":150},[18,17737,17738],{},"任务成功入队后，线程池还会再次检查运行状态。如果线程池在入队期间已经停止，会尝试移除该任务并拒绝；如果池中没有工作线程，则会补充一个工作线程负责消费队列。",[18,17740,17741],{},"这个二次检查避免了任务已经进入队列，却因为线程池状态变化或没有消费者而一直得不到执行。",[14,17743,17745],{"id":17744},"四队列与最大线程数如何相互影响","四、队列与最大线程数如何相互影响",[18,17747,17748],{},"假设配置如下：",[144,17750,17753],{"className":17751,"code":17752,"language":149,"meta":150},[147],"corePoolSize = 8\nmaximumPoolSize = 16\nworkQueue 容量 = 500\n",[22,17754,17752],{"__ignoreMap":150},[18,17756,17757],{},"任务增长过程是：",[144,17759,17762],{"className":17760,"code":17761,"language":149,"meta":150},[147],"前 8 个并发任务\n→ 创建至 8 个核心线程\n\n核心线程忙碌后的新任务\n→ 最多 500 个进入队列\n\n队列满后的新任务\n→ 继续创建线程，最多扩展到 16 个\n\n16 个线程都忙且队列仍满\n→ 触发拒绝策略\n",[22,17763,17761],{"__ignoreMap":150},[18,17765,17766,17767,17769],{},"如果把队列改成默认构造的 ",[22,17768,17524],{},"，它的容量接近无界，任务通常只会继续排队，线程数量很难从 8 扩展到 16。",[18,17771,17772,17773,17775,17776,17778],{},"因此不能孤立地解释 ",[22,17774,17170],{},"，必须和 ",[22,17777,17182],{}," 一起分析。",[14,17780,17782],{"id":17781},"五线程数应该怎么估算","五、线程数应该怎么估算",[473,17784,17786],{"id":17785},"_1-cpu-密集型任务","1. CPU 密集型任务",[18,17788,17789],{},"CPU 密集型任务的大部分时间都在执行计算，例如加密、压缩、图像处理和复杂规则计算。",[18,17791,17792],{},"可以把下面的数值作为初始估算：",[144,17794,17797],{"className":17795,"code":17796,"language":149,"meta":150},[147],"线程数 ≈ CPU 核数 + 1\n",[22,17798,17796],{"__ignoreMap":150},[18,17800,17801],{},"线程数明显超过 CPU 并行能力，通常只会增加上下文切换，并不会线性提升吞吐。",[473,17803,17805],{"id":17804},"_2-io-密集型任务","2. I\u002FO 密集型任务",[18,17807,17808],{},"I\u002FO 密集型任务会花较多时间等待数据库、网络、文件或远程服务，可以用以下公式进行初始估算：",[144,17810,17813],{"className":17811,"code":17812,"language":149,"meta":150},[147],"线程数 ≈ CPU 核数 ×（1 + 平均等待时间 ÷ 平均计算时间）\n",[22,17814,17812],{"__ignoreMap":150},[18,17816,17817],{},"例如任务平均计算 10 毫秒、等待下游 40 毫秒，机器有 8 个 CPU 核：",[144,17819,17822],{"className":17820,"code":17821,"language":149,"meta":150},[147],"8 ×（1 + 40 ÷ 10）= 40\n",[22,17823,17821],{"__ignoreMap":150},[18,17825,17826],{},"40 只能作为起始测试值，不能直接当作生产结论。真实上限还受以下因素约束：",[159,17828,17829,17832,17835,17838,17841],{},[162,17830,17831],{},"数据库连接池只有多少连接；",[162,17833,17834],{},"下游接口允许多少并发；",[162,17836,17837],{},"单任务占用多少内存；",[162,17839,17840],{},"机器 CPU、内存和网络容量；",[162,17842,17843],{},"业务允许的超时和排队时长。",[18,17845,17846],{},"如果数据库连接池只有 20 个可用连接，把线程池配置成 200 个线程，往往只是让更多线程阻塞在等待连接上。",[473,17848,17850],{"id":17849},"_3-队列容量怎么定","3. 队列容量怎么定",[18,17852,17853],{},"队列容量应结合四个维度：",[2695,17855,17856,17859,17862,17865],{},[162,17857,17858],{},"高峰任务到达速率；",[162,17860,17861],{},"线程池稳定处理速率；",[162,17863,17864],{},"业务允许的最大排队时间；",[162,17866,17867],{},"单个排队任务占用的内存。",[18,17869,17870],{},"假设高峰期每秒比稳定处理能力多出 100 个任务，业务最多允许这种峰值持续 3 秒，可以从约 300 个队列容量开始评估：",[144,17872,17875],{"className":17873,"code":17874,"language":149,"meta":150},[147],"暂时积压量\n≈（任务到达速率－稳定处理速率）× 峰值持续时间\n",[22,17876,17874],{"__ignoreMap":150},[18,17878,17879],{},"还必须验证队列排满时的内存占用和最老任务等待时间。容量评估的目标不是永不拒绝，而是在允许的延迟和资源范围内吸收短时突发。",[14,17881,17883],{"id":17882},"六日常推荐写法","六、日常推荐写法",[18,17885,17886],{},"下面是一个边界清晰的业务线程池示例：",[144,17888,17890],{"className":197,"code":17889,"language":199,"meta":150,"style":150},"import java.util.List;\nimport java.util.concurrent.ArrayBlockingQueue;\nimport java.util.concurrent.RejectedExecutionException;\nimport java.util.concurrent.ThreadFactory;\nimport java.util.concurrent.ThreadPoolExecutor;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicInteger;\n\npublic final class OrderAsyncExecutor {\n\n    private static final AtomicInteger THREAD_ID = new AtomicInteger();\n\n    private static final ThreadFactory THREAD_FACTORY = task -> {\n        Thread thread = new Thread(\n                task,\n                \"order-async-\" + THREAD_ID.incrementAndGet()\n        );\n        thread.setUncaughtExceptionHandler((currentThread, error) ->\n                System.err.println(\n                        currentThread.getName() + \": \" + error.getMessage()\n                )\n        );\n        return thread;\n    };\n\n    private static final ThreadPoolExecutor EXECUTOR =\n            new ThreadPoolExecutor(\n                    8,\n                    16,\n                    60L,\n                    TimeUnit.SECONDS,\n                    new ArrayBlockingQueue\u003C>(500),\n                    THREAD_FACTORY,\n                    new ThreadPoolExecutor.AbortPolicy()\n            );\n\n    private OrderAsyncExecutor() {\n    }\n\n    public static void execute(Runnable task) {\n        try {\n            EXECUTOR.execute(task);\n        } catch (RejectedExecutionException exception) {\n            \u002F\u002F 根据业务接入告警、降级、持久化重试或补偿流程\n            throw exception;\n        }\n    }\n\n    public static void shutdown() {\n        EXECUTOR.shutdown();\n        try {\n            if (!EXECUTOR.awaitTermination(30, TimeUnit.SECONDS)) {\n                List\u003CRunnable> notStartedTasks = EXECUTOR.shutdownNow();\n                if (!notStartedTasks.isEmpty()) {\n                    System.err.println(\n                            \"未开始执行的任务数：\" + notStartedTasks.size()\n                    );\n                }\n                if (!EXECUTOR.awaitTermination(30, TimeUnit.SECONDS)) {\n                    System.err.println(\"线程池在中断请求后仍未终止\");\n                }\n            }\n        } catch (InterruptedException exception) {\n            EXECUTOR.shutdownNow();\n            Thread.currentThread().interrupt();\n        }\n    }\n}\n",[22,17891,17892,17900,17907,17914,17921,17928,17935,17942,17946,17960,17964,17987,17992,18013,18028,18034,18051,18057,18071,18081,18107,18113,18118,18127,18133,18138,18152,18162,18170,18178,18186,18192,18205,18211,18223,18228,18233,18243,18248,18253,18273,18281,18291,18306,18312,18321,18327,18332,18337,18351,18362,18369,18393,18414,18432,18442,18458,18464,18470,18489,18503,18508,18514,18527,18536,18550,18555,18560],{"__ignoreMap":150},[203,17893,17894,17897],{"class":205,"line":206},[203,17895,17896],{"class":209},"import",[203,17898,17899],{"class":219}," java.util.List;\n",[203,17901,17902,17904],{"class":205,"line":251},[203,17903,17896],{"class":209},[203,17905,17906],{"class":219}," java.util.concurrent.ArrayBlockingQueue;\n",[203,17908,17909,17911],{"class":205,"line":260},[203,17910,17896],{"class":209},[203,17912,17913],{"class":219}," java.util.concurrent.RejectedExecutionException;\n",[203,17915,17916,17918],{"class":205,"line":278},[203,17917,17896],{"class":209},[203,17919,17920],{"class":219}," java.util.concurrent.ThreadFactory;\n",[203,17922,17923,17925],{"class":205,"line":285},[203,17924,17896],{"class":209},[203,17926,17927],{"class":219}," java.util.concurrent.ThreadPoolExecutor;\n",[203,17929,17930,17932],{"class":205,"line":541},[203,17931,17896],{"class":209},[203,17933,17934],{"class":219}," java.util.concurrent.TimeUnit;\n",[203,17936,17937,17939],{"class":205,"line":549},[203,17938,17896],{"class":209},[203,17940,17941],{"class":219}," java.util.concurrent.atomic.AtomicInteger;\n",[203,17943,17944],{"class":205,"line":787},[203,17945,757],{"emptyLinePlaceholder":756},[203,17947,17948,17950,17953,17955,17958],{"class":205,"line":799},[203,17949,441],{"class":209},[203,17951,17952],{"class":209}," final",[203,17954,1536],{"class":209},[203,17956,17957],{"class":215}," OrderAsyncExecutor",[203,17959,248],{"class":219},[203,17961,17962],{"class":205,"line":1371},[203,17963,757],{"emptyLinePlaceholder":756},[203,17965,17967,17970,17973,17975,17978,17980,17982,17985],{"class":205,"line":17966},11,[203,17968,17969],{"class":209},"    private",[203,17971,17972],{"class":209}," static",[203,17974,17952],{"class":209},[203,17976,17977],{"class":219}," AtomicInteger THREAD_ID ",[203,17979,495],{"class":209},[203,17981,212],{"class":209},[203,17983,17984],{"class":215}," AtomicInteger",[203,17986,257],{"class":219},[203,17988,17990],{"class":205,"line":17989},12,[203,17991,757],{"emptyLinePlaceholder":756},[203,17993,17995,17997,17999,18001,18004,18006,18009,18011],{"class":205,"line":17994},13,[203,17996,17969],{"class":209},[203,17998,17972],{"class":209},[203,18000,17952],{"class":209},[203,18002,18003],{"class":219}," ThreadFactory THREAD_FACTORY ",[203,18005,495],{"class":209},[203,18007,18008],{"class":219}," task ",[203,18010,4891],{"class":209},[203,18012,248],{"class":219},[203,18014,18016,18019,18021,18023,18026],{"class":205,"line":18015},14,[203,18017,18018],{"class":219},"        Thread thread ",[203,18020,495],{"class":209},[203,18022,212],{"class":209},[203,18024,18025],{"class":215}," Thread",[203,18027,1496],{"class":219},[203,18029,18031],{"class":205,"line":18030},15,[203,18032,18033],{"class":219},"                task,\n",[203,18035,18037,18040,18042,18045,18048],{"class":205,"line":18036},16,[203,18038,18039],{"class":223},"                \"order-async-\"",[203,18041,1504],{"class":209},[203,18043,18044],{"class":219}," THREAD_ID.",[203,18046,18047],{"class":215},"incrementAndGet",[203,18049,18050],{"class":219},"()\n",[203,18052,18054],{"class":205,"line":18053},17,[203,18055,18056],{"class":219},"        );\n",[203,18058,18060,18063,18066,18069],{"class":205,"line":18059},18,[203,18061,18062],{"class":219},"        thread.",[203,18064,18065],{"class":215},"setUncaughtExceptionHandler",[203,18067,18068],{"class":219},"((currentThread, error) ",[203,18070,784],{"class":209},[203,18072,18074,18077,18079],{"class":205,"line":18073},19,[203,18075,18076],{"class":219},"                System.err.",[203,18078,3399],{"class":215},[203,18080,1496],{"class":219},[203,18082,18084,18087,18090,18092,18094,18097,18099,18102,18105],{"class":205,"line":18083},20,[203,18085,18086],{"class":219},"                        currentThread.",[203,18088,18089],{"class":215},"getName",[203,18091,16393],{"class":219},[203,18093,11953],{"class":209},[203,18095,18096],{"class":223}," \": \"",[203,18098,1504],{"class":209},[203,18100,18101],{"class":219}," error.",[203,18103,18104],{"class":215},"getMessage",[203,18106,18050],{"class":219},[203,18108,18110],{"class":205,"line":18109},21,[203,18111,18112],{"class":219},"                )\n",[203,18114,18116],{"class":205,"line":18115},22,[203,18117,18056],{"class":219},[203,18119,18121,18124],{"class":205,"line":18120},23,[203,18122,18123],{"class":209},"        return",[203,18125,18126],{"class":219}," thread;\n",[203,18128,18130],{"class":205,"line":18129},24,[203,18131,18132],{"class":219},"    };\n",[203,18134,18136],{"class":205,"line":18135},25,[203,18137,757],{"emptyLinePlaceholder":756},[203,18139,18141,18143,18145,18147,18150],{"class":205,"line":18140},26,[203,18142,17969],{"class":209},[203,18144,17972],{"class":209},[203,18146,17952],{"class":209},[203,18148,18149],{"class":219}," ThreadPoolExecutor EXECUTOR ",[203,18151,16382],{"class":209},[203,18153,18155,18158,18160],{"class":205,"line":18154},27,[203,18156,18157],{"class":209},"            new",[203,18159,17267],{"class":215},[203,18161,1496],{"class":219},[203,18163,18165,18168],{"class":205,"line":18164},28,[203,18166,18167],{"class":718},"                    8",[203,18169,1569],{"class":219},[203,18171,18173,18176],{"class":205,"line":18172},29,[203,18174,18175],{"class":718},"                    16",[203,18177,1569],{"class":219},[203,18179,18181,18184],{"class":205,"line":18180},30,[203,18182,18183],{"class":718},"                    60L",[203,18185,1569],{"class":219},[203,18187,18189],{"class":205,"line":18188},31,[203,18190,18191],{"class":219},"                    TimeUnit.SECONDS,\n",[203,18193,18195,18197,18200,18203],{"class":205,"line":18194},32,[203,18196,790],{"class":209},[203,18198,18199],{"class":219}," ArrayBlockingQueue\u003C>(",[203,18201,18202],{"class":718},"500",[203,18204,12243],{"class":219},[203,18206,18208],{"class":205,"line":18207},33,[203,18209,18210],{"class":219},"                    THREAD_FACTORY,\n",[203,18212,18214,18216,18219,18221],{"class":205,"line":18213},34,[203,18215,790],{"class":209},[203,18217,18218],{"class":219}," ThreadPoolExecutor.",[203,18220,17652],{"class":215},[203,18222,18050],{"class":219},[203,18224,18225],{"class":205,"line":3669},[203,18226,18227],{"class":219},"            );\n",[203,18229,18231],{"class":205,"line":18230},36,[203,18232,757],{"emptyLinePlaceholder":756},[203,18234,18236,18238,18240],{"class":205,"line":18235},37,[203,18237,17969],{"class":209},[203,18239,17957],{"class":215},[203,18241,18242],{"class":219},"() {\n",[203,18244,18246],{"class":205,"line":18245},38,[203,18247,751],{"class":219},[203,18249,18251],{"class":205,"line":18250},39,[203,18252,757],{"emptyLinePlaceholder":756},[203,18254,18256,18258,18260,18262,18265,18268,18271],{"class":205,"line":18255},40,[203,18257,1555],{"class":209},[203,18259,17972],{"class":209},[203,18261,567],{"class":209},[203,18263,18264],{"class":215}," execute",[203,18266,18267],{"class":219},"(Runnable ",[203,18269,18270],{"class":271},"task",[203,18272,275],{"class":219},[203,18274,18276,18279],{"class":205,"line":18275},41,[203,18277,18278],{"class":209},"        try",[203,18280,248],{"class":219},[203,18282,18284,18287,18289],{"class":205,"line":18283},42,[203,18285,18286],{"class":219},"            EXECUTOR.",[203,18288,17721],{"class":215},[203,18290,17724],{"class":219},[203,18292,18294,18297,18299,18302,18304],{"class":205,"line":18293},43,[203,18295,18296],{"class":219},"        } ",[203,18298,28],{"class":209},[203,18300,18301],{"class":219}," (RejectedExecutionException ",[203,18303,272],{"class":271},[203,18305,275],{"class":219},[203,18307,18309],{"class":205,"line":18308},44,[203,18310,18311],{"class":281},"            \u002F\u002F 根据业务接入告警、降级、持久化重试或补偿流程\n",[203,18313,18315,18318],{"class":205,"line":18314},45,[203,18316,18317],{"class":209},"            throw",[203,18319,18320],{"class":219}," exception;\n",[203,18322,18324],{"class":205,"line":18323},46,[203,18325,18326],{"class":219},"        }\n",[203,18328,18330],{"class":205,"line":18329},47,[203,18331,751],{"class":219},[203,18333,18335],{"class":205,"line":18334},48,[203,18336,757],{"emptyLinePlaceholder":756},[203,18338,18340,18342,18344,18346,18349],{"class":205,"line":18339},49,[203,18341,1555],{"class":209},[203,18343,17972],{"class":209},[203,18345,567],{"class":209},[203,18347,18348],{"class":215}," shutdown",[203,18350,18242],{"class":219},[203,18352,18354,18357,18360],{"class":205,"line":18353},50,[203,18355,18356],{"class":219},"        EXECUTOR.",[203,18358,18359],{"class":215},"shutdown",[203,18361,257],{"class":219},[203,18363,18365,18367],{"class":205,"line":18364},51,[203,18366,18278],{"class":209},[203,18368,248],{"class":219},[203,18370,18372,18375,18377,18380,18383,18386,18388,18390],{"class":205,"line":18371},52,[203,18373,18374],{"class":209},"            if",[203,18376,5163],{"class":219},[203,18378,18379],{"class":209},"!",[203,18381,18382],{"class":219},"EXECUTOR.",[203,18384,18385],{"class":215},"awaitTermination",[203,18387,220],{"class":219},[203,18389,14713],{"class":718},[203,18391,18392],{"class":219},", TimeUnit.SECONDS)) {\n",[203,18394,18396,18399,18401,18404,18406,18409,18412],{"class":205,"line":18395},53,[203,18397,18398],{"class":219},"                List\u003C",[203,18400,17307],{"class":209},[203,18402,18403],{"class":219},"> notStartedTasks ",[203,18405,495],{"class":209},[203,18407,18408],{"class":219}," EXECUTOR.",[203,18410,18411],{"class":215},"shutdownNow",[203,18413,257],{"class":219},[203,18415,18417,18420,18422,18424,18427,18430],{"class":205,"line":18416},54,[203,18418,18419],{"class":209},"                if",[203,18421,5163],{"class":219},[203,18423,18379],{"class":209},[203,18425,18426],{"class":219},"notStartedTasks.",[203,18428,18429],{"class":215},"isEmpty",[203,18431,731],{"class":219},[203,18433,18435,18438,18440],{"class":205,"line":18434},55,[203,18436,18437],{"class":219},"                    System.err.",[203,18439,3399],{"class":215},[203,18441,1496],{"class":219},[203,18443,18445,18448,18450,18453,18456],{"class":205,"line":18444},56,[203,18446,18447],{"class":223},"                            \"未开始执行的任务数：\"",[203,18449,1504],{"class":209},[203,18451,18452],{"class":219}," notStartedTasks.",[203,18454,18455],{"class":215},"size",[203,18457,18050],{"class":219},[203,18459,18461],{"class":205,"line":18460},57,[203,18462,18463],{"class":219},"                    );\n",[203,18465,18467],{"class":205,"line":18466},58,[203,18468,18469],{"class":219},"                }\n",[203,18471,18473,18475,18477,18479,18481,18483,18485,18487],{"class":205,"line":18472},59,[203,18474,18419],{"class":209},[203,18476,5163],{"class":219},[203,18478,18379],{"class":209},[203,18480,18382],{"class":219},[203,18482,18385],{"class":215},[203,18484,220],{"class":219},[203,18486,14713],{"class":718},[203,18488,18392],{"class":219},[203,18490,18492,18494,18496,18498,18501],{"class":205,"line":18491},60,[203,18493,18437],{"class":219},[203,18495,3399],{"class":215},[203,18497,220],{"class":219},[203,18499,18500],{"class":223},"\"线程池在中断请求后仍未终止\"",[203,18502,227],{"class":219},[203,18504,18506],{"class":205,"line":18505},61,[203,18507,18469],{"class":219},[203,18509,18511],{"class":205,"line":18510},62,[203,18512,18513],{"class":219},"            }\n",[203,18515,18517,18519,18521,18523,18525],{"class":205,"line":18516},63,[203,18518,18296],{"class":219},[203,18520,28],{"class":209},[203,18522,1843],{"class":219},[203,18524,272],{"class":271},[203,18526,275],{"class":219},[203,18528,18530,18532,18534],{"class":205,"line":18529},64,[203,18531,18286],{"class":219},[203,18533,18411],{"class":215},[203,18535,257],{"class":219},[203,18537,18539,18542,18544,18546,18548],{"class":205,"line":18538},65,[203,18540,18541],{"class":219},"            Thread.",[203,18543,1855],{"class":215},[203,18545,1858],{"class":219},[203,18547,1861],{"class":215},[203,18549,257],{"class":219},[203,18551,18553],{"class":205,"line":18552},66,[203,18554,18326],{"class":219},[203,18556,18558],{"class":205,"line":18557},67,[203,18559,751],{"class":219},[203,18561,18563],{"class":205,"line":18562},68,[203,18564,288],{"class":219},[18,18566,18567],{},"这个示例体现了几个基本原则：",[159,18569,18570,18573,18576,18579,18582,18585],{},[162,18571,18572],{},"线程池被复用，而不是每次请求临时创建；",[162,18574,18575],{},"核心线程、最大线程和队列容量都有明确上限；",[162,18577,18578],{},"线程名能够定位到业务；",[162,18580,18581],{},"拒绝不会被静默忽略；",[162,18583,18584],{},"停止应用时先等待已有任务完成，超时后再中断；",[162,18586,1810,18587,18589],{},[22,18588,645],{}," 后恢复当前线程的中断状态。",[18,18591,18592],{},"实际业务应把告警、指标和补偿接入统一基础设施，而不是只输出错误信息。",[14,18594,18596],{"id":18595},"七为什么不建议盲目使用-executors-工厂方法","七、为什么不建议盲目使用 Executors 工厂方法",[18,18598,18599,18602],{},[22,18600,18601],{},"Executors"," 提供的工厂方法使用方便，但部分方法隐藏了重要的资源边界。",[473,18604,18606],{"id":18605},"_1-newfixedthreadpool-和-newsinglethreadexecutor","1. newFixedThreadPool 和 newSingleThreadExecutor",[18,18608,18609,18610,18612,18613,18615],{},"它们内部使用没有指定容量的 ",[22,18611,17524],{},"。该队列的默认容量是 ",[22,18614,17533],{},"，从业务运行角度看，等同于没有设置可控的任务积压上限：",[144,18617,18620],{"className":18618,"code":18619,"language":149,"meta":150},[147],"生产速度长期大于处理速度\n→ 队列持续增长\n→ 排队延迟和内存占用持续上升\n",[22,18621,18619],{"__ignoreMap":150},[473,18623,18625],{"id":18624},"_2-newcachedthreadpool","2. newCachedThreadPool",[18,18627,18628,18631],{},[22,18629,18630],{},"newCachedThreadPool"," 的主要风险确实来自最大线程数接近无上限。它的关键参数可以简化为：",[144,18633,18636],{"className":18634,"code":18635,"language":149,"meta":150},[147],"corePoolSize = 0\nmaximumPoolSize = Integer.MAX_VALUE\nworkQueue = SynchronousQueue\nkeepAliveTime = 60 秒\n",[22,18637,18635],{"__ignoreMap":150},[18,18639,18640,18642,18643,18645,18646,18648],{},[22,18641,17541],{}," 本身不保存任务。提交任务时，如果没有空闲线程能够接手，线程池就会创建新线程；而 ",[22,18644,17170],{}," 又被设置为 ",[22,18647,17533],{},"，因此任务持续到达或长期阻塞时，线程数量可能快速增长，最终造成大量线程占用内存、频繁上下文切换，甚至耗尽系统资源。",[18,18650,18651],{},"所以它的风险不是任务在队列中无限积压，而是：",[144,18653,18656],{"className":18654,"code":18655,"language":149,"meta":150},[147],"SynchronousQueue 不缓冲任务\n+ maximumPoolSize 接近无上限\n→ 没有空闲线程时持续创建新线程\n",[22,18657,18655],{"__ignoreMap":150},[18,18659,18660,18661,18663],{},"因此在资源敏感的服务端业务中，显式创建 ",[22,18662,17162],{}," 通常更容易表达：",[159,18665,18666,18669,18672],{},[162,18667,18668],{},"最多允许多少并发线程；",[162,18670,18671],{},"最多允许积压多少任务；",[162,18673,18674],{},"超出能力后如何处理。",[18,18676,18677,18678,18680],{},"这并不表示所有 ",[22,18679,18601],{}," 方法都不能使用，而是需要先理解其内部参数和运行边界。",[14,18682,18684],{"id":18683},"八execute-和-submit-有什么区别","八、execute 和 submit 有什么区别",[473,18686,17721],{"id":17721},[144,18688,18689],{"className":197,"code":17712,"language":199,"meta":150,"style":150},[22,18690,18691],{"__ignoreMap":150},[203,18692,18693,18695,18697],{"class":205,"line":206},[203,18694,17344],{"class":219},[203,18696,17721],{"class":215},[203,18698,17724],{"class":219},[159,18700,18701,18706,18709],{},[162,18702,18703,18704,2365],{},"只接收 ",[22,18705,17307],{},[162,18707,18708],{},"没有任务结果；",[162,18710,18711],{},"任务抛出的未捕获异常可以到达线程的未捕获异常处理流程。",[473,18713,18714],{"id":18714},"submit",[144,18716,18718],{"className":197,"code":18717,"language":199,"meta":150,"style":150},"Future\u003CResult> future = threadPool.submit(callable);\n",[22,18719,18720],{"__ignoreMap":150},[203,18721,18722,18725,18728,18731,18733,18736,18738],{"class":205,"line":206},[203,18723,18724],{"class":219},"Future\u003C",[203,18726,18727],{"class":209},"Result",[203,18729,18730],{"class":219},"> future ",[203,18732,495],{"class":209},[203,18734,18735],{"class":219}," threadPool.",[203,18737,18714],{"class":215},[203,18739,18740],{"class":219},"(callable);\n",[159,18742,18743,18751,18757],{},[162,18744,18745,18746,6265,18748,2365],{},"可以接收 ",[22,18747,17307],{},[22,18749,18750],{},"Callable",[162,18752,18753,18754,18756],{},"返回 ",[22,18755,4873],{},"，可以获取结果、等待完成或取消任务；",[162,18758,18759,18760,18762,18763,18766,18767,18770],{},"任务异常会被封装在 ",[22,18761,4873],{}," 中，调用 ",[22,18764,18765],{},"get()"," 时以 ",[22,18768,18769],{},"ExecutionException"," 抛出。",[18,18772,18773,18774,18777,18778,18780,18781,33],{},"如果调用 ",[22,18775,18776],{},"submit()"," 后既不保存 ",[22,18779,4873],{},"，也不检查任务结果，异常很容易被忽略。此时应在任务内部记录异常，或统一检查 ",[22,18782,4873],{},[14,18784,18786],{"id":18785},"九不同类型的任务为什么要隔离线程池","九、不同类型的任务为什么要隔离线程池",[18,18788,18789],{},"把所有异步任务放进同一个线程池，会产生资源相互影响。",[18,18791,686],{},[144,18793,18796],{"className":18794,"code":18795,"language":149,"meta":150},[147],"慢第三方接口占满全部线程\n→ 订单本地计算任务无法执行\n→ 两类本来无关的业务一起超时\n",[22,18797,18795],{"__ignoreMap":150},[18,18799,18800],{},"可以按资源特征或故障域拆分：",[159,18802,18803,18806,18809,18812,18815],{},[162,18804,18805],{},"CPU 计算任务使用计算线程池；",[162,18807,18808],{},"数据库写入任务使用受连接池容量约束的线程池；",[162,18810,18811],{},"慢第三方接口使用独立线程池；",[162,18813,18814],{},"核心交易和非核心通知分开；",[162,18816,18817],{},"不同下游服务使用独立的并发上限。",[18,18819,18820],{},"线程池隔离不是越细越好。线程池过多会增加线程总数和管理成本，应围绕“是否共享同一个资源上限、是否应该相互影响”来划分。",[14,18822,18824],{"id":18823},"十需要监控哪些指标","十、需要监控哪些指标",[18,18826,18827],{},"线程池上线后至少应监控：",[299,18829,18830,18839],{},[302,18831,18832],{},[305,18833,18834,18836],{},[308,18835,7029],{},[308,18837,18838],{},"说明",[315,18840,18841,18852,18863,18874,18882,18890,18901,18909,18917,18925],{},[305,18842,18843,18849],{},[320,18844,18845,18846],{},"当前线程数 ",[22,18847,18848],{},"poolSize",[320,18850,18851],{},"线程池当前拥有的工作线程数量",[305,18853,18854,18860],{},[320,18855,18856,18857],{},"活跃线程数 ",[22,18858,18859],{},"activeCount",[320,18861,18862],{},"正在执行任务的线程数量",[305,18864,18865,18871],{},[320,18866,18867,18868],{},"历史最大线程数 ",[22,18869,18870],{},"largestPoolSize",[320,18872,18873],{},"是否曾经逼近最大线程数",[305,18875,18876,18879],{},[320,18877,18878],{},"队列长度",[320,18880,18881],{},"当前排队任务数量",[305,18883,18884,18887],{},[320,18885,18886],{},"队列剩余容量",[320,18888,18889],{},"距离队列满还有多少空间",[305,18891,18892,18898],{},[320,18893,18894,18895],{},"已完成任务数 ",[22,18896,18897],{},"completedTaskCount",[320,18899,18900],{},"任务是否持续推进",[305,18902,18903,18906],{},[320,18904,18905],{},"拒绝次数",[320,18907,18908],{},"线程池是否已经过载",[305,18910,18911,18914],{},[320,18912,18913],{},"任务排队耗时",[320,18915,18916],{},"任务从提交到开始执行等待了多久",[305,18918,18919,18922],{},[320,18920,18921],{},"任务执行耗时",[320,18923,18924],{},"业务逻辑自身耗时",[305,18926,18927,18930],{},[320,18928,18929],{},"任务成功率与异常数",[320,18931,18932],{},"是否存在持续失败或异常被吞掉",[18,18934,18935],{},"常见告警信号包括：",[159,18937,18938,18941,18944,18947,18950,18953],{},[162,18939,18940],{},"活跃线程数长时间接近最大线程数；",[162,18942,18943],{},"队列长度持续上升而不是短时波动；",[162,18945,18946],{},"排队耗时接近业务超时时间；",[162,18948,18949],{},"拒绝次数持续增长；",[162,18951,18952],{},"已完成任务数停止增长；",[162,18954,18955],{},"下游连接池和线程池同时耗尽。",[18,18957,18958],{},"只监控线程数量不够。很多线程池故障的第一表现不是线程数异常，而是队列等待时间持续增加。",[14,18960,18962],{"id":18961},"十一如何优雅关闭线程池","十一、如何优雅关闭线程池",[18,18964,18965,40,18968,18971],{},[22,18966,18967],{},"shutdown()",[22,18969,18970],{},"shutdownNow()"," 的语义不同：",[144,18973,18976],{"className":18974,"code":18975,"language":149,"meta":150},[147],"shutdown()\n→ 不再接收新任务\n→ 继续执行已提交任务\n\nshutdownNow()\n→ 尝试中断正在执行的任务\n→ 返回队列中尚未开始的任务\n",[22,18977,18975],{"__ignoreMap":150},[18,18979,18980],{},"常见关闭流程是：",[144,18982,18985],{"className":18983,"code":18984,"language":149,"meta":150},[147],"调用 shutdown()\n→ 在限定时间内 awaitTermination()\n→ 超时后调用 shutdownNow()\n→ 记录尚未开始执行的任务\n→ 再次 awaitTermination()，等待已收到中断请求的工作线程退出\n",[22,18986,18984],{"__ignoreMap":150},[18,18988,18989],{},"这里的“再次等待”不是重复做同一件事。",[18,18991,18992,18993,18996,18997,18999],{},"第一次 ",[22,18994,18995],{},"awaitTermination()"," 等待线程池在不接收新任务的前提下，自然执行完已经提交的任务。如果超时，",[22,18998,18970],{}," 会：",[2695,19001,19002,19005],{},[162,19003,19004],{},"尝试中断正在执行的工作线程；",[162,19006,19007],{},"返回队列中尚未开始执行的任务列表。",[18,19009,19010,19011,19013],{},"调用方可以记录或补偿这批尚未开始的任务。随后第二次调用 ",[22,19012,18995],{},"，是给已经收到中断请求的工作线程留出退出时间。如果第二次仍然超时，说明某些任务可能没有正确响应中断，需要记录告警并进一步排查。",[18,19015,13674,19016,19018],{},[22,19017,18970],{}," 只能发出中断请求，不能强制终止线程。如果任务忽略中断，线程池仍可能无法及时结束。",[18,19020,19021,19022,19024],{},"任务代码还必须正确响应中断。阻塞调用收到 ",[22,19023,645],{}," 后，不应随意吞掉中断状态。",[14,19026,19028],{"id":19027},"十二常见误区","十二、常见误区",[473,19030,19032],{"id":19031},"_1-最大线程数越大处理能力越强","1. 最大线程数越大，处理能力越强",[18,19034,19035],{},"线程数量超过 CPU、数据库连接池或下游承载能力后，只会增加竞争、等待和上下文切换。",[473,19037,19039],{"id":19038},"_2-队列越大越不容易出问题","2. 队列越大，越不容易出问题",[18,19041,19042],{},"大队列可能只是把拒绝转化为更长的延迟和更高的内存风险。",[473,19044,19046],{"id":19045},"_3-使用-callerrunspolicy-就解决了限流","3. 使用 CallerRunsPolicy 就解决了限流",[18,19048,19049,19051],{},[22,19050,17667],{}," 会让提交线程执行任务，从而减慢生产者，但它不是严格的 QPS 限流器。调用线程可能是 Web 请求线程、消息消费线程或定时调度线程，执行任务会影响这些上游组件的延迟和可用性。",[473,19053,19055],{"id":19054},"_4-threadpoolexecutor-线程安全任务就一定线程安全","4. ThreadPoolExecutor 线程安全，任务就一定线程安全",[18,19057,19058],{},"线程池只负责安全调度任务，不会自动保护任务访问的共享对象。任务内部仍要正确使用锁、原子类、并发容器或不可变对象。",[473,19060,19062],{"id":19061},"_5-submit-后没有异常日志就表示任务成功","5. submit 后没有异常日志，就表示任务成功",[18,19064,19065,19067,19068,19070,19071,19073],{},[22,19066,18776],{}," 会把异常保存到 ",[22,19069,4873],{},"。如果从未调用 ",[22,19072,18765],{}," 或没有统一异常处理，失败可能没有明显日志。",[473,19075,19077],{"id":19076},"_6-线程池能够无限吸收突发流量","6. 线程池能够无限吸收突发流量",[18,19079,19080],{},"线程池只能在有限线程和有限队列范围内吸收波动。持续过载必须通过限流、降级、削峰、扩容或优化下游能力解决。",[14,19082,19084],{"id":19083},"十三日常使用检查清单","十三、日常使用检查清单",[159,19086,19087,19090,19093,19096,19099,19102,19105,19108,19116,19123],{},[162,19088,19089],{},"是否按照任务类型和下游资源进行合理隔离；",[162,19091,19092],{},"是否明确设置线程数和有界队列；",[162,19094,19095],{},"线程数量是否与 CPU、连接池和下游容量匹配；",[162,19097,19098],{},"是否设置可识别的线程名称；",[162,19100,19101],{},"拒绝策略是否符合业务语义；",[162,19103,19104],{},"被拒绝任务是否有告警、降级或补偿；",[162,19106,19107],{},"是否记录排队耗时、执行耗时和拒绝次数；",[162,19109,19110,19112,19113,19115],{},[22,19111,18776],{}," 返回的 ",[22,19114,4873],{}," 是否被正确处理；",[162,19117,19118,19119,19122],{},"任务是否支持中断，是否会泄漏 ",[22,19120,19121],{},"ThreadLocal"," 上下文；",[162,19124,19125],{},"应用关闭时是否执行线程池优雅停止。",[14,19127,5803],{"id":5803},[159,19129,19130,19137,19144],{},[162,19131,19132],{},[119,19133,19136],{"href":19134,"rel":19135},"https:\u002F\u002Fdocs.oracle.com\u002Fjavase\u002F8\u002Fdocs\u002Fapi\u002Fjava\u002Futil\u002Fconcurrent\u002FThreadPoolExecutor.html",[2080],"ThreadPoolExecutor Java 8 API",[162,19138,19139],{},[119,19140,19143],{"href":19141,"rel":19142},"https:\u002F\u002Fdocs.oracle.com\u002Fjavase\u002F8\u002Fdocs\u002Fapi\u002Fjava\u002Futil\u002Fconcurrent\u002FExecutors.html",[2080],"Executors Java 8 API",[162,19145,19146],{},[119,19147,19150],{"href":19148,"rel":19149},"https:\u002F\u002Fdocs.oracle.com\u002Fjavase\u002F8\u002Fdocs\u002Fapi\u002Fjava\u002Futil\u002Fconcurrent\u002FRejectedExecutionHandler.html",[2080],"RejectedExecutionHandler Java 8 API",[2111,19152,19153],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":150,"searchDepth":251,"depth":251,"links":19155},[19156,19157,19158,19159,19160,19169,19170,19171,19176,19177,19181,19185,19186,19187,19188,19196,19197],{"id":16,"depth":251,"text":16},{"id":115,"depth":251,"text":115},{"id":132,"depth":251,"text":132},{"id":17221,"depth":251,"text":17222},{"id":17251,"depth":251,"text":17252,"children":19161},[19162,19163,19164,19165,19166,19167,19168],{"id":17325,"depth":260,"text":17326},{"id":17378,"depth":260,"text":17379},{"id":17401,"depth":260,"text":17402},{"id":17446,"depth":260,"text":17447},{"id":17477,"depth":260,"text":17478},{"id":17586,"depth":260,"text":17587},{"id":17621,"depth":260,"text":17622},{"id":17705,"depth":251,"text":17706},{"id":17744,"depth":251,"text":17745},{"id":17781,"depth":251,"text":17782,"children":19172},[19173,19174,19175],{"id":17785,"depth":260,"text":17786},{"id":17804,"depth":260,"text":17805},{"id":17849,"depth":260,"text":17850},{"id":17882,"depth":251,"text":17883},{"id":18595,"depth":251,"text":18596,"children":19178},[19179,19180],{"id":18605,"depth":260,"text":18606},{"id":18624,"depth":260,"text":18625},{"id":18683,"depth":251,"text":18684,"children":19182},[19183,19184],{"id":17721,"depth":260,"text":17721},{"id":18714,"depth":260,"text":18714},{"id":18785,"depth":251,"text":18786},{"id":18823,"depth":251,"text":18824},{"id":18961,"depth":251,"text":18962},{"id":19027,"depth":251,"text":19028,"children":19189},[19190,19191,19192,19193,19194,19195],{"id":19031,"depth":260,"text":19032},{"id":19038,"depth":260,"text":19039},{"id":19045,"depth":260,"text":19046},{"id":19054,"depth":260,"text":19055},{"id":19061,"depth":260,"text":19062},{"id":19076,"depth":260,"text":19077},{"id":19083,"depth":251,"text":19084},{"id":5803,"depth":251,"text":5803},"wiki:java:java-thread-pool-core-parameters-and-practice","系统讲解 ThreadPoolExecutor 七个核心参数、任务提交流程、阻塞队列与拒绝策略选择，并给出线程池配置、监控、关闭和容量评估的日常实践。",{},79,"\u002Fjava\u002Fjava-thread-pool-core-parameters-and-practice",{"title":17150,"description":19199},"java\u002Fjava-thread-pool-core-parameters-and-practice","2026-07-27","di9Q_z1vSXT1dkyqU0bFHy7k2NFzb6V7vRHzB-lGKx4",{"id":19208,"title":19209,"body":19210,"commentId":21896,"description":21897,"difficulty":3667,"draft":2148,"extension":2149,"meta":21898,"navigation":756,"order":21899,"path":21900,"section":2153,"seo":21901,"stem":21902,"updated":19205,"__hash__":21903},"java\u002Fjava\u002Fspring-bean-lifecycle-and-extension-points.md","Spring Bean 生命周期有哪些阶段？都有哪些扩展点？",{"type":7,"value":19211,"toc":21835},[19212,19215,19217,19223,19241,19266,19280,19311,19313,19320,19322,19331,19333,19337,19340,19344,19347,19353,19357,19360,19366,19374,19378,19383,19406,19409,19435,19438,19442,19448,19450,19461,19467,19471,19479,19481,19492,19496,19505,19557,19563,19577,19594,19598,19601,19606,19648,19651,19654,19671,19678,19691,19694,19704,19708,19714,19717,19818,19821,19857,19860,19863,19869,19879,19883,19890,19894,19899,19902,19908,19911,19915,19924,19977,19980,19986,19989,19995,20002,20216,20228,20234,20303,20309,20313,20316,20378,20384,20388,20391,20412,20418,20422,20425,20429,20432,20438,20441,20452,20463,20467,20470,20476,20479,20485,20487,20501,20505,20512,20523,20529,20533,20539,20545,20549,20555,20558,20577,20580,20584,20587,20590,20674,20684,20687,20691,20694,20708,20710,20724,20728,20736,20739,20745,20749,20752,20770,20772,20883,20886,20946,20952,20955,20966,20969,20973,20976,20989,20992,21006,21012,21015,21021,21030,21034,21037,21041,21044,21077,21079,21094,21097,21101,21106,21109,21144,21147,21150,21154,21159,21221,21224,21241,21247,21251,21254,21257,21281,21289,21292,21309,21313,21320,21323,21329,21333,21471,21475,21482,21574,21580,21586,21593,21597,21600,21626,21636,21639,21656,21660,21664,21667,21671,21676,21680,21683,21687,21693,21697,21700,21704,21715,21719,21722,21728,21731,21737,21741,21793,21795,21832],[10,19213,19209],{"id":19214},"spring-bean-生命周期有哪些阶段都有哪些扩展点",[14,19216,16],{"id":16},[18,19218,19219,19220,33],{},"Spring Bean 的完整生命周期可以分为：",[110,19221,19222],{},"Bean 定义注册与修改、实例化、属性填充、Aware 回调、初始化前处理、初始化、初始化后处理、Bean 就绪，以及容器关闭时的销毁",[18,19224,19225,19226,33,19229,19232,19233,19236,19237,19240],{},"容器启动后，首先读取配置并注册 ",[22,19227,19228],{},"BeanDefinition",[22,19230,19231],{},"BeanDefinitionRegistryPostProcessor"," 可以继续注册或删除定义，",[22,19234,19235],{},"BeanFactoryPostProcessor"," 可以在 Bean 创建前修改定义。随后 Spring 注册所有 ",[22,19238,19239],{},"BeanPostProcessor","，再创建非懒加载单例。",[18,19242,19243,19244,19247,19248,53,19251,53,19254,19257,19258,19261,19262,19265],{},"创建单个 Bean 时，Spring先选择构造方法并实例化对象，再进行依赖注入；随后执行 ",[22,19245,19246],{},"Aware"," 回调、",[22,19249,19250],{},"BeanPostProcessor#postProcessBeforeInitialization()",[22,19252,19253],{},"@PostConstruct",[22,19255,19256],{},"InitializingBean#afterPropertiesSet()"," 和自定义 ",[22,19259,19260],{},"init-method","，最后执行 ",[22,19263,19264],{},"postProcessAfterInitialization()","。Spring AOP 通常在初始化后处理阶段返回代理对象。",[18,19267,19268,19269,19272,19273,19276,19277,19279],{},"这条主线还有一个特殊分支：目标对象实例化之前，",[22,19270,19271],{},"InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation()"," 可以返回替代对象。返回 ",[22,19274,19275],{},"null"," 才会继续正常创建；返回非 ",[22,19278,19275],{}," 时会跳过原 Bean 的实例化、属性填充和初始化，并对替代对象执行初始化后处理。",[18,19281,19282,19283,19286,19287,19290,19291,19294,19295,19298,19299,19302,19303,19306,19307,19310],{},"所有普通非懒加载单例创建完成后，可以使用 ",[22,19284,19285],{},"SmartInitializingSingleton"," 或监听 ",[22,19288,19289],{},"ContextRefreshedEvent"," 执行全局初始化；需要管理组件启动和停止顺序时，可以使用 ",[22,19292,19293],{},"SmartLifecycle","。容器关闭时，先由 ",[22,19296,19297],{},"DestructionAwareBeanPostProcessor"," 执行销毁前处理，其中 ",[22,19300,19301],{},"CommonAnnotationBeanPostProcessor"," 会调用 ",[22,19304,19305],{},"@PreDestroy","；之后再执行 ",[22,19308,19309],{},"DisposableBean#destroy()"," 和自定义销毁方法。",[18,19312,103],{},[105,19314,19315],{},[18,19316,19317],{},[110,19318,19319],{},"Spring 先处理 BeanDefinition，再通过 BeanPostProcessor 贯穿 Bean 的创建过程；Bean 初始化完成不等于容器全部就绪，批量就绪、运行启停和销毁分别有对应扩展点。",[14,19321,115],{"id":115},[18,19323,19324],{},[119,19325,19328],{"href":19326,"target":122,"rel":19327},"\u002Fimages\u002Fwiki\u002Fjava\u002Fspring-bean-lifecycle-and-extension-points.svg",[124,125],[127,19329],{"src":19326,"alt":19330},"Spring 容器启动、Bean 创建、就绪运行和销毁阶段及主要扩展点",[14,19332,132],{"id":132},[14,19334,19336],{"id":19335},"一先区分两个生命周期","一、先区分两个生命周期",[18,19338,19339],{},"讨论“Spring 生命周期”时，容易把两个概念混在一起：",[473,19341,19343],{"id":19342},"_1-applicationcontext-容器生命周期","1. ApplicationContext 容器生命周期",[18,19345,19346],{},"容器负责：",[144,19348,19351],{"className":19349,"code":19350,"language":149,"meta":150},[147],"加载配置\n→ 注册和修改 BeanDefinition\n→ 注册容器级后处理器\n→ 创建非懒加载单例\n→ 发布刷新完成事件\n→ 启动 Lifecycle 组件\n→ 关闭并销毁单例\n",[22,19352,19350],{"__ignoreMap":150},[473,19354,19356],{"id":19355},"_2-单个-bean-的生命周期","2. 单个 Bean 的生命周期",[18,19358,19359],{},"单个 Bean 经历：",[144,19361,19364],{"className":19362,"code":19363,"language":149,"meta":150},[147],"实例化\n→ 属性填充\n→ Aware 回调\n→ 初始化前处理\n→ 初始化回调\n→ 初始化后处理\n→ 对外提供\n→ 销毁\n",[22,19365,19363],{"__ignoreMap":150},[18,19367,19368,19370,19371,19373],{},[22,19369,19235],{}," 作用于整个容器中的 Bean 定义；",[22,19372,19239],{}," 作用于容器创建的 Bean 实例。两者所处层次不同。",[14,19375,19377],{"id":19376},"二阶段一加载并注册-beandefinition","二、阶段一：加载并注册 BeanDefinition",[18,19379,19380,19382],{},[22,19381,19228],{}," 是 Spring 创建 Bean 的配置模型，记录了：",[159,19384,19385,19388,19391,19394,19397,19400,19403],{},[162,19386,19387],{},"Bean 类型；",[162,19389,19390],{},"作用域；",[162,19392,19393],{},"是否懒加载；",[162,19395,19396],{},"构造参数和属性值；",[162,19398,19399],{},"初始化方法和销毁方法；",[162,19401,19402],{},"依赖关系；",[162,19404,19405],{},"是否允许作为自动注入候选项。",[18,19407,19408],{},"Bean 定义可能来自：",[159,19410,19411,19417,19423,19426,19429],{},[162,19412,19413,19416],{},[22,19414,19415],{},"@Component"," 组件扫描；",[162,19418,19419,19422],{},[22,19420,19421],{},"@Bean"," 方法；",[162,19424,19425],{},"XML；",[162,19427,19428],{},"程序化注册；",[162,19430,19431,19434],{},[22,19432,19433],{},"@Import"," 导入。",[18,19436,19437],{},"这一阶段还没有创建普通业务 Bean，处理的是“如何创建 Bean”的元数据。",[473,19439,19441],{"id":19440},"_1-importselector","1. ImportSelector",[18,19443,19444,19447],{},[22,19445,19446],{},"ImportSelector"," 根据导入类的元数据，返回需要注册的配置类名称。",[18,19449,4332],{},[159,19451,19452,19455,19458],{},[162,19453,19454],{},"按条件组合一组配置；",[162,19456,19457],{},"模块化装配；",[162,19459,19460],{},"框架自动配置入口。",[18,19462,19463,19466],{},[22,19464,19465],{},"DeferredImportSelector"," 会在普通配置类处理之后再决定导入内容，Spring Boot 自动配置大量使用了这类机制。",[473,19468,19470],{"id":19469},"_2-importbeandefinitionregistrar","2. ImportBeanDefinitionRegistrar",[18,19472,19473,19476,19477,33],{},[22,19474,19475],{},"ImportBeanDefinitionRegistrar"," 可以直接向注册表写入 ",[22,19478,19228],{},[18,19480,4332],{},[159,19482,19483,19486,19489],{},[162,19484,19485],{},"根据注解属性动态注册 Bean；",[162,19487,19488],{},"为接口生成代理 BeanDefinition；",[162,19490,19491],{},"注册数量或名称在编译期不固定的组件。",[473,19493,19495],{"id":19494},"_3-beandefinitionregistrypostprocessor","3. BeanDefinitionRegistryPostProcessor",[18,19497,19498,19499,19501,19502,431],{},"它是 ",[22,19500,19235],{}," 的子接口，核心能力是操作 ",[22,19503,19504],{},"BeanDefinitionRegistry",[144,19506,19508],{"className":197,"code":19507,"language":199,"meta":150,"style":150},"public interface BeanDefinitionRegistryPostProcessor\n        extends BeanFactoryPostProcessor {\n\n    void postProcessBeanDefinitionRegistry(\n            BeanDefinitionRegistry registry);\n}\n",[22,19509,19510,19520,19529,19533,19543,19553],{"__ignoreMap":150},[203,19511,19512,19514,19517],{"class":205,"line":206},[203,19513,441],{"class":209},[203,19515,19516],{"class":209}," interface",[203,19518,19519],{"class":215}," BeanDefinitionRegistryPostProcessor\n",[203,19521,19522,19524,19527],{"class":205,"line":251},[203,19523,1671],{"class":209},[203,19525,19526],{"class":215}," BeanFactoryPostProcessor",[203,19528,248],{"class":219},[203,19530,19531],{"class":205,"line":260},[203,19532,757],{"emptyLinePlaceholder":756},[203,19534,19535,19538,19541],{"class":205,"line":278},[203,19536,19537],{"class":209},"    void",[203,19539,19540],{"class":215}," postProcessBeanDefinitionRegistry",[203,19542,1496],{"class":219},[203,19544,19545,19548,19551],{"class":205,"line":285},[203,19546,19547],{"class":219},"            BeanDefinitionRegistry ",[203,19549,19550],{"class":271},"registry",[203,19552,227],{"class":219},[203,19554,19555],{"class":205,"line":541},[203,19556,288],{"class":219},[18,19558,19559,19560,19562],{},"它可以在普通 ",[22,19561,19235],{}," 执行前：",[159,19564,19565,19568,19571,19574],{},[162,19566,19567],{},"注册新的 BeanDefinition；",[162,19569,19570],{},"删除或替换已有 BeanDefinition；",[162,19572,19573],{},"扫描额外的组件；",[162,19575,19576],{},"根据外部配置动态扩展容器。",[18,19578,19579,19582,19583,53,19586,53,19589,40,19591,19593],{},[22,19580,19581],{},"ConfigurationClassPostProcessor"," 就是重要实现，它负责解析 ",[22,19584,19585],{},"@Configuration",[22,19587,19588],{},"@ComponentScan",[22,19590,19433],{},[22,19592,19421],{}," 等配置。",[14,19595,19597],{"id":19596},"三阶段二修改-beandefinition","三、阶段二：修改 BeanDefinition",[473,19599,19235],{"id":19600},"beanfactorypostprocessor",[18,19602,19603,19604,431],{},"所有 BeanDefinition 基本注册完成后，Spring 调用 ",[22,19605,19235],{},[144,19607,19609],{"className":197,"code":19608,"language":199,"meta":150,"style":150},"public interface BeanFactoryPostProcessor {\n\n    void postProcessBeanFactory(\n            ConfigurableListableBeanFactory beanFactory);\n}\n",[22,19610,19611,19621,19625,19634,19644],{"__ignoreMap":150},[203,19612,19613,19615,19617,19619],{"class":205,"line":206},[203,19614,441],{"class":209},[203,19616,19516],{"class":209},[203,19618,19526],{"class":215},[203,19620,248],{"class":219},[203,19622,19623],{"class":205,"line":251},[203,19624,757],{"emptyLinePlaceholder":756},[203,19626,19627,19629,19632],{"class":205,"line":260},[203,19628,19537],{"class":209},[203,19630,19631],{"class":215}," postProcessBeanFactory",[203,19633,1496],{"class":219},[203,19635,19636,19639,19642],{"class":205,"line":278},[203,19637,19638],{"class":219},"            ConfigurableListableBeanFactory ",[203,19640,19641],{"class":271},"beanFactory",[203,19643,227],{"class":219},[203,19645,19646],{"class":205,"line":285},[203,19647,288],{"class":219},[18,19649,19650],{},"它处理的是 BeanDefinition，而不是已经创建好的业务对象。",[18,19652,19653],{},"典型能力包括：",[159,19655,19656,19659,19662,19665,19668],{},[162,19657,19658],{},"修改属性值；",[162,19660,19661],{},"修改作用域和懒加载配置；",[162,19663,19664],{},"调整自动注入候选状态；",[162,19666,19667],{},"解析配置占位符；",[162,19669,19670],{},"注册容器级基础设施。",[18,19672,19673,19674,19677],{},"例如 ",[22,19675,19676],{},"PropertySourcesPlaceholderConfigurer"," 会把：",[144,19679,19681],{"className":4755,"code":19680,"language":4757,"meta":150,"style":150},"order.timeout=${ORDER_TIMEOUT}\n",[22,19682,19683],{"__ignoreMap":150},[203,19684,19685,19688],{"class":205,"line":206},[203,19686,19687],{"class":209},"order.timeout",[203,19689,19690],{"class":219},"=${ORDER_TIMEOUT}\n",[18,19692,19693],{},"中的占位符替换为环境中的实际值。",[18,19695,19696,19697,19700,19701,19703],{},"不要在这个阶段随意调用 ",[22,19698,19699],{},"getBean()","。这会导致普通 Bean 提前创建，使它可能绕过尚未注册完成的 ",[22,19702,19239],{},"，进而错过依赖注入、AOP 等处理。",[14,19705,19707],{"id":19706},"四阶段三注册-beanpostprocessor","四、阶段三：注册 BeanPostProcessor",[18,19709,19710,19711,19713],{},"Spring 会提前创建并注册所有 ",[22,19712,19239],{},"，使它们能够处理后续创建的 Bean。",[18,19715,19716],{},"基础接口如下：",[144,19718,19720],{"className":197,"code":19719,"language":199,"meta":150,"style":150},"public interface BeanPostProcessor {\n\n    default Object postProcessBeforeInitialization(\n            Object bean, String beanName) {\n        return bean;\n    }\n\n    default Object postProcessAfterInitialization(\n            Object bean, String beanName) {\n        return bean;\n    }\n}\n",[22,19721,19722,19733,19737,19750,19766,19773,19777,19781,19792,19804,19810,19814],{"__ignoreMap":150},[203,19723,19724,19726,19728,19731],{"class":205,"line":206},[203,19725,441],{"class":209},[203,19727,19516],{"class":209},[203,19729,19730],{"class":215}," BeanPostProcessor",[203,19732,248],{"class":219},[203,19734,19735],{"class":205,"line":251},[203,19736,757],{"emptyLinePlaceholder":756},[203,19738,19739,19742,19745,19748],{"class":205,"line":260},[203,19740,19741],{"class":209},"    default",[203,19743,19744],{"class":219}," Object ",[203,19746,19747],{"class":215},"postProcessBeforeInitialization",[203,19749,1496],{"class":219},[203,19751,19752,19755,19758,19761,19764],{"class":205,"line":278},[203,19753,19754],{"class":219},"            Object ",[203,19756,19757],{"class":271},"bean",[203,19759,19760],{"class":219},", String ",[203,19762,19763],{"class":271},"beanName",[203,19765,275],{"class":219},[203,19767,19768,19770],{"class":205,"line":285},[203,19769,18123],{"class":209},[203,19771,19772],{"class":219}," bean;\n",[203,19774,19775],{"class":205,"line":541},[203,19776,751],{"class":219},[203,19778,19779],{"class":205,"line":549},[203,19780,757],{"emptyLinePlaceholder":756},[203,19782,19783,19785,19787,19790],{"class":205,"line":787},[203,19784,19741],{"class":209},[203,19786,19744],{"class":219},[203,19788,19789],{"class":215},"postProcessAfterInitialization",[203,19791,1496],{"class":219},[203,19793,19794,19796,19798,19800,19802],{"class":205,"line":799},[203,19795,19754],{"class":219},[203,19797,19757],{"class":271},[203,19799,19760],{"class":219},[203,19801,19763],{"class":271},[203,19803,275],{"class":219},[203,19805,19806,19808],{"class":205,"line":1371},[203,19807,18123],{"class":209},[203,19809,19772],{"class":219},[203,19811,19812],{"class":205,"line":17966},[203,19813,751],{"class":219},[203,19815,19816],{"class":205,"line":17989},[203,19817,288],{"class":219},[18,19819,19820],{},"常见功能很多都建立在 BeanPostProcessor 体系上：",[159,19822,19823,19832,19838,19841,19846,19851,19854],{},[162,19824,19825,53,19828,19831],{},[22,19826,19827],{},"@Autowired",[22,19829,19830],{},"@Value"," 注入；",[162,19833,19834,53,19836,2365],{},[22,19835,19253],{},[22,19837,19305],{},[162,19839,19840],{},"AOP 自动代理；",[162,19842,19843,2365],{},[22,19844,19845],{},"@Async",[162,19847,19848,2365],{},[22,19849,19850],{},"@Scheduled",[162,19852,19853],{},"事务代理；",[162,19855,19856],{},"异常转换。",[473,19858,19859],{"id":19859},"执行顺序",[18,19861,19862],{},"多个后处理器通常按照以下优先级排序：",[144,19864,19867],{"className":19865,"code":19866,"language":149,"meta":150},[147],"PriorityOrdered\n→ Ordered\n→ 未实现排序接口\n",[22,19868,19866],{"__ignoreMap":150},[18,19870,19871,19872,19875,19876,33],{},"程序化调用 ",[22,19873,19874],{},"addBeanPostProcessor()"," 注册时，主要按照实际注册顺序执行，不应只依赖 ",[22,19877,19878],{},"Ordered",[14,19880,19882],{"id":19881},"五阶段四实例化-bean","五、阶段四：实例化 Bean",[18,19884,19885,19886,19889],{},"Spring 在 ",[22,19887,19888],{},"AbstractAutowireCapableBeanFactory#createBean()"," 中进入单个 Bean 的创建流程。",[473,19891,19893],{"id":19892},"_1-实例化前扩展","1. 实例化前扩展",[18,19895,19896,19898],{},[22,19897,19271],{}," 在目标对象真正实例化前执行。",[18,19900,19901],{},"它可以直接返回一个对象，例如代理：",[144,19903,19906],{"className":19904,"code":19905,"language":149,"meta":150},[147],"返回 null\n→ 继续正常实例化流程\n\n返回非 null\n→ 跳过普通实例化、属性填充和初始化流程\n→ 对返回对象执行初始化后处理\n",[22,19907,19905],{"__ignoreMap":150},[18,19909,19910],{},"这属于特殊扩展点，主要用于框架基础设施，不适合承载普通业务初始化。",[473,19912,19914],{"id":19913},"_2-为什么同时存在两个-before-回调","2. 为什么同时存在两个 Before 回调",[18,19916,19917,40,19920,19923],{},[22,19918,19919],{},"postProcessBeforeInstantiation()",[22,19921,19922],{},"postProcessBeforeInitialization()"," 名称相似，但位置、作用和接收到的参数完全不同：",[299,19925,19926,19942],{},[302,19927,19928],{},[305,19929,19930,19933,19936,19939],{},[308,19931,19932],{},"回调",[308,19934,19935],{},"所属接口",[308,19937,19938],{},"执行位置",[308,19940,19941],{},"此时原 Bean 是否存在",[315,19943,19944,19961],{},[305,19945,19946,19950,19955,19958],{},[320,19947,19948],{},[22,19949,19919],{},[320,19951,19952],{},[22,19953,19954],{},"InstantiationAwareBeanPostProcessor",[320,19956,19957],{},"构造方法或工厂方法执行前",[320,19959,19960],{},"不存在",[305,19962,19963,19967,19971,19974],{},[320,19964,19965],{},[22,19966,19922],{},[320,19968,19969],{},[22,19970,19239],{},[320,19972,19973],{},"实例化、属性填充和 Aware 回调之后，初始化方法之前",[320,19975,19976],{},"已经存在",[18,19978,19979],{},"正常创建路径如下：",[144,19981,19984],{"className":19982,"code":19983,"language":149,"meta":150},[147],"postProcessBeforeInstantiation 返回 null\n→ 选择构造方法并实例化\n→ postProcessAfterInstantiation\n→ postProcessProperties\n→ 属性填充\n→ Aware 回调\n→ postProcessBeforeInitialization\n   → @PostConstruct\n→ afterPropertiesSet\n→ 自定义 init-method\n→ postProcessAfterInitialization\n",[22,19985,19983],{"__ignoreMap":150},[18,19987,19988],{},"短路路径则是：",[144,19990,19993],{"className":19991,"code":19992,"language":149,"meta":150},[147],"postProcessBeforeInstantiation 返回非 null 对象\n→ 跳过原 Bean 的实例化、属性填充和初始化\n→ 对返回对象执行 postProcessAfterInitialization\n→ 返回对象成为最终 Bean\n",[22,19994,19992],{"__ignoreMap":150},[18,19996,19997,19998,20001],{},"下面的处理器会在 ",[22,19999,20000],{},"GreetingServiceImpl"," 实例化之前直接返回一个 JDK 动态代理：",[144,20003,20005],{"className":197,"code":20004,"language":199,"meta":150,"style":150},"@Component\npublic class GreetingInstantiationPostProcessor\n        implements InstantiationAwareBeanPostProcessor {\n\n    @Override\n    public Object postProcessBeforeInstantiation(\n            Class\u003C?> beanClass,\n            String beanName) {\n\n        if (beanClass != GreetingServiceImpl.class) {\n            return null;\n        }\n\n        return Proxy.newProxyInstance(\n                beanClass.getClassLoader(),\n                beanClass.getInterfaces(),\n                (proxy, method, args) -> {\n                    if (\"hello\".equals(method.getName())) {\n                        return \"hello from proxy\";\n                    }\n                    return null;\n                });\n    }\n}\n",[22,20006,20007,20015,20024,20034,20038,20046,20057,20073,20081,20085,20098,20107,20111,20115,20127,20138,20147,20156,20179,20189,20194,20203,20208,20212],{"__ignoreMap":150},[203,20008,20009,20012],{"class":205,"line":206},[203,20010,20011],{"class":219},"@",[203,20013,20014],{"class":209},"Component\n",[203,20016,20017,20019,20021],{"class":205,"line":251},[203,20018,441],{"class":209},[203,20020,1536],{"class":209},[203,20022,20023],{"class":215}," GreetingInstantiationPostProcessor\n",[203,20025,20026,20029,20032],{"class":205,"line":260},[203,20027,20028],{"class":209},"        implements",[203,20030,20031],{"class":215}," InstantiationAwareBeanPostProcessor",[203,20033,248],{"class":219},[203,20035,20036],{"class":205,"line":278},[203,20037,757],{"emptyLinePlaceholder":756},[203,20039,20040,20043],{"class":205,"line":285},[203,20041,20042],{"class":219},"    @",[203,20044,20045],{"class":209},"Override\n",[203,20047,20048,20050,20052,20055],{"class":205,"line":541},[203,20049,1555],{"class":209},[203,20051,19744],{"class":219},[203,20053,20054],{"class":215},"postProcessBeforeInstantiation",[203,20056,1496],{"class":219},[203,20058,20059,20062,20065,20068,20071],{"class":205,"line":549},[203,20060,20061],{"class":219},"            Class\u003C",[203,20063,20064],{"class":209},"?",[203,20066,20067],{"class":219},"> ",[203,20069,20070],{"class":271},"beanClass",[203,20072,1569],{"class":219},[203,20074,20075,20077,20079],{"class":205,"line":787},[203,20076,1564],{"class":219},[203,20078,19763],{"class":271},[203,20080,275],{"class":219},[203,20082,20083],{"class":205,"line":799},[203,20084,757],{"emptyLinePlaceholder":756},[203,20086,20087,20090,20093,20095],{"class":205,"line":1371},[203,20088,20089],{"class":209},"        if",[203,20091,20092],{"class":219}," (beanClass ",[203,20094,1350],{"class":209},[203,20096,20097],{"class":219}," GreetingServiceImpl.class) {\n",[203,20099,20100,20103,20105],{"class":205,"line":17966},[203,20101,20102],{"class":209},"            return",[203,20104,719],{"class":718},[203,20106,1297],{"class":219},[203,20108,20109],{"class":205,"line":17989},[203,20110,18326],{"class":219},[203,20112,20113],{"class":205,"line":17994},[203,20114,757],{"emptyLinePlaceholder":756},[203,20116,20117,20119,20122,20125],{"class":205,"line":18015},[203,20118,18123],{"class":209},[203,20120,20121],{"class":219}," Proxy.",[203,20123,20124],{"class":215},"newProxyInstance",[203,20126,1496],{"class":219},[203,20128,20129,20132,20135],{"class":205,"line":18030},[203,20130,20131],{"class":219},"                beanClass.",[203,20133,20134],{"class":215},"getClassLoader",[203,20136,20137],{"class":219},"(),\n",[203,20139,20140,20142,20145],{"class":205,"line":18036},[203,20141,20131],{"class":219},[203,20143,20144],{"class":215},"getInterfaces",[203,20146,20137],{"class":219},[203,20148,20149,20152,20154],{"class":205,"line":18053},[203,20150,20151],{"class":219},"                (proxy, method, args) ",[203,20153,4891],{"class":209},[203,20155,248],{"class":219},[203,20157,20158,20161,20163,20166,20168,20171,20174,20176],{"class":205,"line":18059},[203,20159,20160],{"class":209},"                    if",[203,20162,5163],{"class":219},[203,20164,20165],{"class":223},"\"hello\"",[203,20167,9766],{"class":219},[203,20169,20170],{"class":215},"equals",[203,20172,20173],{"class":219},"(method.",[203,20175,18089],{"class":215},[203,20177,20178],{"class":219},"())) {\n",[203,20180,20181,20184,20187],{"class":205,"line":18073},[203,20182,20183],{"class":209},"                        return",[203,20185,20186],{"class":223}," \"hello from proxy\"",[203,20188,1297],{"class":219},[203,20190,20191],{"class":205,"line":18083},[203,20192,20193],{"class":219},"                    }\n",[203,20195,20196,20199,20201],{"class":205,"line":18109},[203,20197,20198],{"class":209},"                    return",[203,20200,719],{"class":718},[203,20202,1297],{"class":219},[203,20204,20205],{"class":205,"line":18115},[203,20206,20207],{"class":219},"                });\n",[203,20209,20210],{"class":205,"line":18120},[203,20211,751],{"class":219},[203,20213,20214],{"class":205,"line":18129},[203,20215,288],{"class":219},[18,20217,9031,20218,20220,20221,20224,20225,20227],{},[22,20219,20000],{}," 实现了 ",[22,20222,20223],{},"GreetingService"," 接口，那么容器最终保存的是代理对象，原类的构造方法和 ",[22,20226,19253],{}," 都不会执行。使用 JDK 动态代理时应通过接口类型获取或注入该 Bean，不能把最终对象强制转换为实现类。",[18,20229,20230,20231,20233],{},"如果这个方法返回 ",[22,20232,19275],{},"，Spring才会继续创建原对象：",[144,20235,20237],{"className":197,"code":20236,"language":199,"meta":150,"style":150},"@Override\npublic Object postProcessBeforeInstantiation(\n        Class\u003C?> beanClass,\n        String beanName) {\n\n    System.out.println(\"实例化前：\" + beanName);\n    return null;\n}\n",[22,20238,20239,20245,20255,20266,20271,20275,20291,20299],{"__ignoreMap":150},[203,20240,20241,20243],{"class":205,"line":206},[203,20242,20011],{"class":219},[203,20244,20045],{"class":209},[203,20246,20247,20249,20251,20253],{"class":205,"line":251},[203,20248,441],{"class":209},[203,20250,19744],{"class":219},[203,20252,20054],{"class":215},[203,20254,1496],{"class":219},[203,20256,20257,20260,20263],{"class":205,"line":260},[203,20258,20259],{"class":219},"        Class",[203,20261,20262],{"class":209},"\u003C?>",[203,20264,20265],{"class":219}," beanClass,\n",[203,20267,20268],{"class":205,"line":278},[203,20269,20270],{"class":219},"        String beanName) {\n",[203,20272,20273],{"class":205,"line":285},[203,20274,757],{"emptyLinePlaceholder":756},[203,20276,20277,20279,20281,20283,20286,20288],{"class":205,"line":541},[203,20278,3430],{"class":219},[203,20280,3399],{"class":215},[203,20282,220],{"class":219},[203,20284,20285],{"class":223},"\"实例化前：\"",[203,20287,1504],{"class":209},[203,20289,20290],{"class":219}," beanName);\n",[203,20292,20293,20295,20297],{"class":205,"line":549},[203,20294,455],{"class":209},[203,20296,719],{"class":718},[203,20298,1297],{"class":219},[203,20300,20301],{"class":205,"line":787},[203,20302,288],{"class":219},[18,20304,20305,20306,20308],{},"还要注意，普通 Spring AOP 通常在 ",[22,20307,19264],{}," 中创建代理。实例化前直接返回代理属于特殊的短路能力，不能把它理解为所有 AOP 代理的常规创建位置。",[473,20310,20312],{"id":20311},"_3-instantiationawarebeanpostprocessor-不只在实例化前工作","3. InstantiationAwareBeanPostProcessor 不只在实例化前工作",[18,20314,20315],{},"这个接口名称表示它能够感知 Bean 的实例化过程，但它的回调分布在多个阶段：",[299,20317,20318,20328],{},[302,20319,20320],{},[305,20321,20322,20325],{},[308,20323,20324],{},"方法",[308,20326,20327],{},"执行阶段",[315,20329,20330,20339,20349,20359,20369],{},[305,20331,20332,20336],{},[320,20333,20334],{},[22,20335,19919],{},[320,20337,20338],{},"目标对象实例化前",[305,20340,20341,20346],{},[320,20342,20343],{},[22,20344,20345],{},"postProcessAfterInstantiation()",[320,20347,20348],{},"目标对象实例化后、属性填充前",[305,20350,20351,20356],{},[320,20352,20353],{},[22,20354,20355],{},"postProcessProperties()",[320,20357,20358],{},"属性填充阶段",[305,20360,20361,20366],{},[320,20362,20363,20364],{},"继承的 ",[22,20365,19922],{},[320,20367,20368],{},"初始化方法前",[305,20370,20371,20375],{},[320,20372,20363,20373],{},[22,20374,19264],{},[320,20376,20377],{},"初始化方法后",[18,20379,20380,20381,20383],{},"因此，准确的说法不是“",[22,20382,19954],{}," 只在实例化前执行”，而是“它提供了实例化前、实例化后和属性填充阶段的扩展能力，同时继承了 BeanPostProcessor 的初始化前后回调”。",[473,20385,20387],{"id":20386},"_4-选择构造方法","4. 选择构造方法",[18,20389,20390],{},"Spring需要决定使用：",[159,20392,20393,20396,20399,20404,20407],{},[162,20394,20395],{},"默认构造方法；",[162,20397,20398],{},"显式指定的构造方法；",[162,20400,20401,20403],{},[22,20402,19827],{}," 构造方法；",[162,20405,20406],{},"工厂方法；",[162,20408,20409,33],{},[22,20410,20411],{},"Supplier",[18,20413,20414,20417],{},[22,20415,20416],{},"SmartInstantiationAwareBeanPostProcessor#determineCandidateConstructors()"," 可以参与候选构造方法选择。",[473,20419,20421],{"id":20420},"_5-创建原始对象","5. 创建原始对象",[18,20423,20424],{},"构造方法或工厂方法执行完成后，得到的是原始 Bean 实例。此时依赖属性通常还没有填充，初始化回调也没有执行。",[14,20426,20428],{"id":20427},"六阶段五处理合并后的-beandefinition","六、阶段五：处理合并后的 BeanDefinition",[18,20430,20431],{},"父子 BeanDefinition、配置覆盖和类型解析完成后，Spring 得到合并后的 BeanDefinition。",[18,20433,20434,20437],{},[22,20435,20436],{},"MergedBeanDefinitionPostProcessor#postProcessMergedBeanDefinition()"," 可以缓存与具体 Bean 类型有关的元数据。",[18,20439,20440],{},"常见用途包括：",[159,20442,20443,20446,20449],{},[162,20444,20445],{},"扫描注入点；",[162,20447,20448],{},"缓存注解元数据；",[162,20450,20451],{},"为后续属性注入和销毁处理做准备。",[18,20453,20454,20457,20458,53,20460,20462],{},[22,20455,20456],{},"AutowiredAnnotationBeanPostProcessor"," 会在这里分析 ",[22,20459,19827],{},[22,20461,19830],{}," 等注入信息，避免每次都重新反射扫描。",[14,20464,20466],{"id":20465},"七阶段六提前暴露引用","七、阶段六：提前暴露引用",[18,20468,20469],{},"对于允许循环依赖处理的单例，Spring可能在属性填充前暴露一个对象工厂：",[144,20471,20474],{"className":20472,"code":20473,"language":149,"meta":150},[147],"singletonFactories\n→ earlySingletonObjects\n→ singletonObjects\n",[22,20475,20473],{"__ignoreMap":150},[18,20477,20478],{},"这里暴露的不是“已经完成初始化的 Bean”，而是用于打破依赖环的早期引用。",[18,20480,20481,20484],{},[22,20482,20483],{},"SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference()"," 可以决定早期暴露什么对象。AOP 自动代理创建器会利用这个扩展点，尽量让循环依赖中的其他 Bean 获得代理引用，而不是直接获得原始对象。",[18,20486,4831],{},[159,20488,20489,20492,20495,20498],{},[162,20490,20491],{},"这主要处理单例的属性或 Setter 注入循环依赖；",[162,20493,20494],{},"构造器循环依赖无法靠提前暴露一个尚未构造完成的对象解决；",[162,20496,20497],{},"原型 Bean 的循环依赖也不能通过三级缓存解决；",[162,20499,20500],{},"不应把三级缓存当成设计循环依赖的理由。",[14,20502,20504],{"id":20503},"八阶段七属性填充与依赖注入","八、阶段七：属性填充与依赖注入",[18,20506,20507,20508,20511],{},"实例创建后，Spring进入 ",[22,20509,20510],{},"populateBean()","，为对象填充依赖。",[18,20513,20514,20516,20517,20519,20520,20522],{},[22,20515,20510],{}," 是这一阶段的主流程入口，不要把它与 ",[22,20518,20355],{}," 混为一谈。前者负责组织整个属性填充阶段，后者只是 ",[22,20521,19954],{}," 在该阶段提供的扩展回调。其主要调用关系可以简化为：",[144,20524,20527],{"className":20525,"code":20526,"language":149,"meta":150},[147],"populateBean\n├── postProcessAfterInstantiation\n├── postProcessProperties\n└── 应用 BeanDefinition 中配置的属性值\n",[22,20528,20526],{"__ignoreMap":150},[473,20530,20532],{"id":20531},"_1-postprocessafterinstantiation","1. postProcessAfterInstantiation",[18,20534,20535,20538],{},[22,20536,20537],{},"InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation()"," 在对象已经实例化、属性填充尚未开始时执行。",[18,20540,18753,20541,20544],{},[22,20542,20543],{},"false"," 可以阻止 Spring继续执行常规属性填充，因此属于影响较大的底层扩展点。",[473,20546,20548],{"id":20547},"_2-postprocessproperties","2. postProcessProperties",[18,20550,20551,20554],{},[22,20552,20553],{},"InstantiationAwareBeanPostProcessor#postProcessProperties()"," 可以处理属性注入。",[18,20556,20557],{},"典型实现：",[159,20559,20560,20569],{},[162,20561,20562,20564,20565,40,20567,2365],{},[22,20563,20456],{},"：处理 ",[22,20566,19827],{},[22,20568,19830],{},[162,20570,20571,20564,20573,20576],{},[22,20572,19301],{},[22,20574,20575],{},"@Resource"," 等 Jakarta 注解。",[18,20578,20579],{},"随后 Spring还会应用 BeanDefinition 中显式配置的属性值。",[14,20581,20583],{"id":20582},"九阶段八aware-回调","九、阶段八：Aware 回调",[18,20585,20586],{},"依赖注入完成后，Spring让 Bean 感知容器提供的基础设施。",[18,20588,20589],{},"常见接口包括：",[299,20591,20592,20602],{},[302,20593,20594],{},[305,20595,20596,20599],{},[308,20597,20598],{},"Aware 接口",[308,20600,20601],{},"能够获得的对象",[315,20603,20604,20614,20624,20634,20644,20654,20664],{},[305,20605,20606,20611],{},[320,20607,20608],{},[22,20609,20610],{},"BeanNameAware",[320,20612,20613],{},"当前 Bean 名称",[305,20615,20616,20621],{},[320,20617,20618],{},[22,20619,20620],{},"BeanClassLoaderAware",[320,20622,20623],{},"Bean 类加载器",[305,20625,20626,20631],{},[320,20627,20628],{},[22,20629,20630],{},"BeanFactoryAware",[320,20632,20633],{},"当前 BeanFactory",[305,20635,20636,20641],{},[320,20637,20638],{},[22,20639,20640],{},"EnvironmentAware",[320,20642,20643],{},"Environment",[305,20645,20646,20651],{},[320,20647,20648],{},[22,20649,20650],{},"ApplicationEventPublisherAware",[320,20652,20653],{},"事件发布器",[305,20655,20656,20661],{},[320,20657,20658],{},[22,20659,20660],{},"ResourceLoaderAware",[320,20662,20663],{},"资源加载器",[305,20665,20666,20671],{},[320,20667,20668],{},[22,20669,20670],{},"ApplicationContextAware",[320,20672,20673],{},"ApplicationContext",[18,20675,20676,20677,20679,20680,20683],{},"前三个由 BeanFactory 初始化流程直接调用；",[22,20678,20670],{}," 等接口通常由 ",[22,20681,20682],{},"ApplicationContextAwareProcessor"," 这个 BeanPostProcessor 处理。",[18,20685,20686],{},"Aware 适合获取容器基础设施，但会让业务类与 Spring 接口耦合。普通业务依赖仍应优先采用构造器注入。",[14,20688,20690],{"id":20689},"十阶段九初始化前处理","十、阶段九：初始化前处理",[18,20692,20693],{},"Spring依次调用所有：",[144,20695,20697],{"className":197,"code":20696,"language":199,"meta":150,"style":150},"BeanPostProcessor#postProcessBeforeInitialization()\n",[22,20698,20699],{"__ignoreMap":150},[203,20700,20701,20704,20706],{"class":205,"line":206},[203,20702,20703],{"class":219},"BeanPostProcessor#",[203,20705,19747],{"class":215},[203,20707,18050],{"class":219},[18,20709,20440],{},[159,20711,20712,20715,20718,20721],{},[162,20713,20714],{},"识别并执行生命周期注解；",[162,20716,20717],{},"对 Bean 做初始化前检查；",[162,20719,20720],{},"注入容器上下文相关能力；",[162,20722,20723],{},"包装或替换 Bean。",[473,20725,20727],{"id":20726},"postconstruct-在哪里执行","@PostConstruct 在哪里执行",[18,20729,20730,20732,20733,20735],{},[22,20731,19253],{}," 不是 Spring硬编码在初始化流程中的独立步骤，而是由 ",[22,20734,19301],{}," 在初始化前处理阶段识别并调用。",[18,20737,20738],{},"因此从宏观顺序看：",[144,20740,20743],{"className":20741,"code":20742,"language":149,"meta":150},[147],"Aware 回调\n→ postProcessBeforeInitialization\n   → @PostConstruct\n→ InitializingBean\n→ 自定义 init-method\n",[22,20744,20742],{"__ignoreMap":150},[14,20746,20748],{"id":20747},"十一阶段十执行初始化回调","十一、阶段十：执行初始化回调",[18,20750,20751],{},"同一个 Bean 同时使用多种初始化机制且方法名不同时，调用顺序是：",[2695,20753,20754,20758,20762],{},[162,20755,20756,2365],{},[22,20757,19253],{},[162,20759,20760,2365],{},[22,20761,19256],{},[162,20763,20764,20767,20768,33],{},[22,20765,20766],{},"@Bean(initMethod = \"...\")"," 或 XML ",[22,20769,19260],{},[18,20771,3368],{},[144,20773,20775],{"className":197,"code":20774,"language":199,"meta":150,"style":150},"public class PaymentClient implements InitializingBean {\n\n    @PostConstruct\n    public void validateConfiguration() {\n        \u002F\u002F 校验配置\n    }\n\n    @Override\n    public void afterPropertiesSet() {\n        \u002F\u002F Spring 接口回调\n    }\n\n    public void init() {\n        \u002F\u002F 自定义初始化方法\n    }\n}\n",[22,20776,20777,20794,20798,20805,20816,20821,20825,20829,20835,20846,20851,20855,20859,20870,20875,20879],{"__ignoreMap":150},[203,20778,20779,20781,20783,20786,20789,20792],{"class":205,"line":206},[203,20780,441],{"class":209},[203,20782,1536],{"class":209},[203,20784,20785],{"class":215}," PaymentClient",[203,20787,20788],{"class":209}," implements",[203,20790,20791],{"class":215}," InitializingBean",[203,20793,248],{"class":219},[203,20795,20796],{"class":205,"line":251},[203,20797,757],{"emptyLinePlaceholder":756},[203,20799,20800,20802],{"class":205,"line":260},[203,20801,20042],{"class":219},[203,20803,20804],{"class":209},"PostConstruct\n",[203,20806,20807,20809,20811,20814],{"class":205,"line":278},[203,20808,1555],{"class":209},[203,20810,567],{"class":209},[203,20812,20813],{"class":215}," validateConfiguration",[203,20815,18242],{"class":219},[203,20817,20818],{"class":205,"line":285},[203,20819,20820],{"class":281},"        \u002F\u002F 校验配置\n",[203,20822,20823],{"class":205,"line":541},[203,20824,751],{"class":219},[203,20826,20827],{"class":205,"line":549},[203,20828,757],{"emptyLinePlaceholder":756},[203,20830,20831,20833],{"class":205,"line":787},[203,20832,20042],{"class":219},[203,20834,20045],{"class":209},[203,20836,20837,20839,20841,20844],{"class":205,"line":799},[203,20838,1555],{"class":209},[203,20840,567],{"class":209},[203,20842,20843],{"class":215}," afterPropertiesSet",[203,20845,18242],{"class":219},[203,20847,20848],{"class":205,"line":1371},[203,20849,20850],{"class":281},"        \u002F\u002F Spring 接口回调\n",[203,20852,20853],{"class":205,"line":17966},[203,20854,751],{"class":219},[203,20856,20857],{"class":205,"line":17989},[203,20858,757],{"emptyLinePlaceholder":756},[203,20860,20861,20863,20865,20868],{"class":205,"line":17994},[203,20862,1555],{"class":209},[203,20864,567],{"class":209},[203,20866,20867],{"class":215}," init",[203,20869,18242],{"class":219},[203,20871,20872],{"class":205,"line":18015},[203,20873,20874],{"class":281},"        \u002F\u002F 自定义初始化方法\n",[203,20876,20877],{"class":205,"line":18030},[203,20878,751],{"class":219},[203,20880,20881],{"class":205,"line":18036},[203,20882,288],{"class":219},[18,20884,20885],{},"配置：",[144,20887,20889],{"className":197,"code":20888,"language":199,"meta":150,"style":150},"@Bean(initMethod = \"init\", destroyMethod = \"close\")\npublic PaymentClient paymentClient() {\n    return new PaymentClient();\n}\n",[22,20890,20891,20920,20932,20942],{"__ignoreMap":150},[203,20892,20893,20895,20898,20900,20903,20905,20908,20910,20913,20915,20918],{"class":205,"line":206},[203,20894,20011],{"class":219},[203,20896,20897],{"class":209},"Bean",[203,20899,220],{"class":219},[203,20901,20902],{"class":718},"initMethod",[203,20904,9514],{"class":209},[203,20906,20907],{"class":223}," \"init\"",[203,20909,5179],{"class":219},[203,20911,20912],{"class":718},"destroyMethod",[203,20914,9514],{"class":209},[203,20916,20917],{"class":223}," \"close\"",[203,20919,12343],{"class":219},[203,20921,20922,20924,20927,20930],{"class":205,"line":251},[203,20923,441],{"class":209},[203,20925,20926],{"class":219}," PaymentClient ",[203,20928,20929],{"class":215},"paymentClient",[203,20931,18242],{"class":219},[203,20933,20934,20936,20938,20940],{"class":205,"line":260},[203,20935,455],{"class":209},[203,20937,212],{"class":209},[203,20939,20785],{"class":215},[203,20941,257],{"class":219},[203,20943,20944],{"class":205,"line":278},[203,20945,288],{"class":219},[18,20947,20948,20949,20951],{},"业务代码通常优先选择 ",[22,20950,19253],{}," 或自定义初始化方法，避免为了生命周期回调而强耦合 Spring 接口。",[18,20953,20954],{},"初始化方法适合：",[159,20956,20957,20960,20963],{},[162,20958,20959],{},"校验必要配置；",[162,20961,20962],{},"构造只依赖当前 Bean 状态的内存结构；",[162,20964,20965],{},"完成本地资源准备。",[18,20967,20968],{},"不适合在初始化方法中启动复杂异步任务，或者访问大量其他 Bean。初始化发生在单例创建过程中，过重或存在交叉访问的逻辑可能延长启动时间，甚至引发初始化死锁。",[14,20970,20972],{"id":20971},"十二阶段十一初始化后处理与-aop-代理","十二、阶段十一：初始化后处理与 AOP 代理",[18,20974,20975],{},"初始化回调完成后，Spring调用：",[144,20977,20979],{"className":197,"code":20978,"language":199,"meta":150,"style":150},"BeanPostProcessor#postProcessAfterInitialization()\n",[22,20980,20981],{"__ignoreMap":150},[203,20982,20983,20985,20987],{"class":205,"line":206},[203,20984,20703],{"class":219},[203,20986,19789],{"class":215},[203,20988,18050],{"class":219},[18,20990,20991],{},"后处理器可以返回：",[159,20993,20994,20997,21000,21003],{},[162,20995,20996],{},"原对象；",[162,20998,20999],{},"包装对象；",[162,21001,21002],{},"代理对象；",[162,21004,21005],{},"其他兼容对象。",[18,21007,21008,21009,21011],{},"Spring AOP 的自动代理创建器属于 ",[22,21010,19239],{},"。它会判断当前 Bean 是否匹配 Advisor；如果匹配，就创建 JDK 动态代理或 CGLIB 代理。",[18,21013,21014],{},"因此容器最终对外暴露的对象可能不是原始对象：",[144,21016,21019],{"className":21017,"code":21018,"language":149,"meta":150},[147],"原始 Bean\n→ 初始化回调\n→ AOP 后处理器\n→ 代理 Bean\n→ singletonObjects\n",[22,21020,21018],{"__ignoreMap":150},[18,21022,21023,21024,21026,21027,21029],{},"这也解释了为什么在 ",[22,21025,19253],{}," 中调用自身的事务方法，通常不会经过当前 Bean 的事务代理：初始化回调执行时，完整代理尚未作为最终 Bean 对外提供，而且调用方式仍然是对象内部的 ",[22,21028,2286],{}," 调用。",[14,21031,21033],{"id":21032},"十三bean-初始化完成后容器何时才算就绪","十三、Bean 初始化完成后，容器何时才算就绪",[18,21035,21036],{},"单个 Bean 完成初始化，并不代表所有单例都已经创建完成。",[473,21038,21040],{"id":21039},"_1-smartinitializingsingleton","1. SmartInitializingSingleton",[18,21042,21043],{},"所有常规非懒加载单例完成实例化后，Spring调用：",[144,21045,21047],{"className":197,"code":21046,"language":199,"meta":150,"style":150},"public interface SmartInitializingSingleton {\n\n    void afterSingletonsInstantiated();\n}\n",[22,21048,21049,21060,21064,21073],{"__ignoreMap":150},[203,21050,21051,21053,21055,21058],{"class":205,"line":206},[203,21052,441],{"class":209},[203,21054,19516],{"class":209},[203,21056,21057],{"class":215}," SmartInitializingSingleton",[203,21059,248],{"class":219},[203,21061,21062],{"class":205,"line":251},[203,21063,757],{"emptyLinePlaceholder":756},[203,21065,21066,21068,21071],{"class":205,"line":260},[203,21067,19537],{"class":209},[203,21069,21070],{"class":215}," afterSingletonsInstantiated",[203,21072,257],{"class":219},[203,21074,21075],{"class":205,"line":278},[203,21076,288],{"class":219},[18,21078,4332],{},[159,21080,21081,21084,21087],{},[162,21082,21083],{},"需要确认其他单例已经准备完成的检查；",[162,21085,21086],{},"汇总多个 Bean 建立注册表；",[162,21088,21089,21090,21093],{},"避免因为主动 ",[22,21091,21092],{},"getBeansOfType()"," 导致意外提前初始化。",[18,21095,21096],{},"它只对单例 Bean 有意义。",[473,21098,21100],{"id":21099},"_2-contextrefreshedevent","2. ContextRefreshedEvent",[18,21102,21103,21104,33],{},"容器刷新完成后会发布 ",[22,21105,19289],{},[18,21107,21108],{},"可以使用：",[144,21110,21112],{"className":197,"code":21111,"language":199,"meta":150,"style":150},"@EventListener(ContextRefreshedEvent.class)\npublic void onContextReady() {\n    \u002F\u002F 容器刷新完成后的逻辑\n}\n",[22,21113,21114,21124,21135,21140],{"__ignoreMap":150},[203,21115,21116,21118,21121],{"class":205,"line":206},[203,21117,20011],{"class":219},[203,21119,21120],{"class":209},"EventListener",[203,21122,21123],{"class":219},"(ContextRefreshedEvent.class)\n",[203,21125,21126,21128,21130,21133],{"class":205,"line":251},[203,21127,441],{"class":209},[203,21129,567],{"class":209},[203,21131,21132],{"class":215}," onContextReady",[203,21134,18242],{"class":219},[203,21136,21137],{"class":205,"line":260},[203,21138,21139],{"class":281},"    \u002F\u002F 容器刷新完成后的逻辑\n",[203,21141,21142],{"class":205,"line":278},[203,21143,288],{"class":219},[18,21145,21146],{},"适合需要基于 ApplicationContext 刷新完成事件触发的逻辑。",[18,21148,21149],{},"需要注意，ApplicationContext 可以被刷新多次时，监听逻辑也可能执行多次，业务代码应考虑幂等。",[473,21151,21153],{"id":21152},"_3-smartlifecycle","3. SmartLifecycle",[18,21155,21156,21157,431],{},"需要启动和停止后台消费者、调度器、长连接等有运行状态的组件时，优先考虑 ",[22,21158,19293],{},[144,21160,21162],{"className":197,"code":21161,"language":199,"meta":150,"style":150},"public interface SmartLifecycle extends Lifecycle, Phased {\n\n    boolean isAutoStartup();\n\n    void stop(Runnable callback);\n}\n",[22,21163,21164,21185,21189,21199,21203,21217],{"__ignoreMap":150},[203,21165,21166,21168,21170,21173,21175,21178,21180,21183],{"class":205,"line":206},[203,21167,441],{"class":209},[203,21169,19516],{"class":209},[203,21171,21172],{"class":215}," SmartLifecycle",[203,21174,1541],{"class":209},[203,21176,21177],{"class":215}," Lifecycle",[203,21179,5179],{"class":219},[203,21181,21182],{"class":215},"Phased",[203,21184,248],{"class":219},[203,21186,21187],{"class":205,"line":251},[203,21188,757],{"emptyLinePlaceholder":756},[203,21190,21191,21194,21197],{"class":205,"line":260},[203,21192,21193],{"class":209},"    boolean",[203,21195,21196],{"class":215}," isAutoStartup",[203,21198,257],{"class":219},[203,21200,21201],{"class":205,"line":278},[203,21202,757],{"emptyLinePlaceholder":756},[203,21204,21205,21207,21210,21212,21215],{"class":205,"line":285},[203,21206,19537],{"class":209},[203,21208,21209],{"class":215}," stop",[203,21211,18267],{"class":219},[203,21213,21214],{"class":271},"callback",[203,21216,227],{"class":219},[203,21218,21219],{"class":205,"line":541},[203,21220,288],{"class":219},[18,21222,21223],{},"它支持：",[159,21225,21226,21229,21235,21238],{},[162,21227,21228],{},"容器刷新后自动启动；",[162,21230,21231,21234],{},[22,21232,21233],{},"phase"," 控制启动和停止顺序；",[162,21236,21237],{},"异步停止完成回调；",[162,21239,21240],{},"运行状态检查。",[18,21242,21243,21244,21246],{},"启动时通常先启动较小的 ",[22,21245,21233],{},"，关闭时反向停止。依赖其他组件的服务应根据依赖关系设计 phase，而不是随意填写。",[14,21248,21250],{"id":21249},"十四阶段十二容器关闭与-bean-销毁","十四、阶段十二：容器关闭与 Bean 销毁",[18,21252,21253],{},"容器正常关闭时，Spring先停止需要运行期管理的 Lifecycle 组件，再销毁受管理的单例 Bean。",[18,21255,21256],{},"同一个 Bean 配置多种销毁机制且方法名不同时，可以按下面的层次理解：",[2695,21258,21259,21269,21273],{},[162,21260,21261,19298,21264,21266,21267,2365],{},[22,21262,21263],{},"DestructionAwareBeanPostProcessor#postProcessBeforeDestruction()",[22,21265,19301],{}," 调用 ",[22,21268,19305],{},[162,21270,21271,2365],{},[22,21272,19309],{},[162,21274,21275,20767,21278,33],{},[22,21276,21277],{},"@Bean(destroyMethod = \"...\")",[22,21279,21280],{},"destroy-method",[18,21282,14473,21283,21285,21286,21288],{},[22,21284,19305],{}," 不是脱离后处理器单独运行的一轮流程，而是 ",[22,21287,19297],{}," 体系提供的具体生命周期回调。",[18,21290,21291],{},"销毁阶段适合：",[159,21293,21294,21297,21300,21303,21306],{},[162,21295,21296],{},"停止后台任务；",[162,21298,21299],{},"关闭线程池；",[162,21301,21302],{},"关闭网络连接；",[162,21304,21305],{},"刷新缓冲数据；",[162,21307,21308],{},"注销服务或监听器。",[473,21310,21312],{"id":21311},"原型-bean-的特殊点","原型 Bean 的特殊点",[18,21314,21315,21316,21319],{},"对于 ",[22,21317,21318],{},"prototype"," Bean，Spring负责创建和注入，但把实例交给调用方后通常不再跟踪其完整生命周期，因此不会自动调用普通销毁回调。",[18,21321,21322],{},"如果原型 Bean 持有必须释放的资源，应由使用方显式关闭，或者改用更合适的作用域和资源管理方式。",[18,21324,21325,21326,21328],{},"应用被强制终止时，销毁回调也不保证一定执行。重要数据不能只依赖 ",[22,21327,19305],{}," 才落盘。",[14,21330,21332],{"id":21331},"十五常见扩展点应该怎么选择","十五、常见扩展点应该怎么选择",[299,21334,21335,21344],{},[302,21336,21337],{},[305,21338,21339,21341],{},[308,21340,6623],{},[308,21342,21343],{},"优先考虑的扩展点",[315,21345,21346,21357,21368,21377,21387,21398,21410,21419,21429,21438,21449,21459],{},[305,21347,21348,21351],{},[320,21349,21350],{},"导入一组配置类",[320,21352,21353,53,21355],{},[22,21354,19446],{},[22,21356,19465],{},[305,21358,21359,21362],{},[320,21360,21361],{},"动态注册 BeanDefinition",[320,21363,21364,53,21366],{},[22,21365,19475],{},[22,21367,19231],{},[305,21369,21370,21373],{},[320,21371,21372],{},"在创建 Bean 前修改配置元数据",[320,21374,21375],{},[22,21376,19235],{},[305,21378,21379,21382],{},[320,21380,21381],{},"自定义注入、实例化或代理",[320,21383,21384,21386],{},[22,21385,19239],{}," 及其子接口",[305,21388,21389,21392],{},[320,21390,21391],{},"获得 Bean 名称或容器对象",[320,21393,21394,21395,21397],{},"对应的 ",[22,21396,19246],{}," 接口",[305,21399,21400,21403],{},[320,21401,21402],{},"依赖注入完成后校验当前 Bean",[320,21404,21405,21407,21408],{},[22,21406,19253],{}," 或自定义 ",[22,21409,20902],{},[305,21411,21412,21415],{},[320,21413,21414],{},"所有普通单例创建完成后执行",[320,21416,21417],{},[22,21418,19285],{},[305,21420,21421,21424],{},[320,21422,21423],{},"容器刷新完成后执行",[320,21425,21426,21427],{},"监听 ",[22,21428,19289],{},[305,21430,21431,21434],{},[320,21432,21433],{},"管理后台组件的启动和停止",[320,21435,21436],{},[22,21437,19293],{},[305,21439,21440,21443],{},[320,21441,21442],{},"释放 Bean 自身资源",[320,21444,21445,21407,21447],{},[22,21446,19305],{},[22,21448,20912],{},[305,21450,21451,21454],{},[320,21452,21453],{},"创建复杂对象并把产物交给容器",[320,21455,21456],{},[22,21457,21458],{},"FactoryBean",[305,21460,21461,21464],{},[320,21462,21463],{},"定义新的 Bean 作用域",[320,21465,21466,21467,21470],{},"实现 ",[22,21468,21469],{},"Scope"," 并注册",[14,21472,21474],{"id":21473},"十六factorybean-是什么扩展点","十六、FactoryBean 是什么扩展点",[18,21476,21477,21478,21481],{},"普通 BeanDefinition 描述“这个类本身如何被创建”；",[22,21479,21480],{},"FactoryBean\u003CT>"," 描述“通过这个工厂生产什么对象”。",[144,21483,21485],{"className":197,"code":21484,"language":199,"meta":150,"style":150},"public interface FactoryBean\u003CT> {\n\n    T getObject() throws Exception;\n\n    Class\u003C?> getObjectType();\n\n    default boolean isSingleton() {\n        return true;\n    }\n}\n",[22,21486,21487,21504,21508,21523,21527,21541,21545,21557,21566,21570],{"__ignoreMap":150},[203,21488,21489,21491,21493,21496,21498,21501],{"class":205,"line":206},[203,21490,441],{"class":209},[203,21492,19516],{"class":209},[203,21494,21495],{"class":215}," FactoryBean",[203,21497,3306],{"class":219},[203,21499,21500],{"class":209},"T",[203,21502,21503],{"class":219},"> {\n",[203,21505,21506],{"class":205,"line":251},[203,21507,757],{"emptyLinePlaceholder":756},[203,21509,21510,21513,21516,21518,21520],{"class":205,"line":260},[203,21511,21512],{"class":219},"    T ",[203,21514,21515],{"class":215},"getObject",[203,21517,16393],{"class":219},[203,21519,86],{"class":209},[203,21521,21522],{"class":219}," Exception;\n",[203,21524,21525],{"class":205,"line":278},[203,21526,757],{"emptyLinePlaceholder":756},[203,21528,21529,21532,21534,21536,21539],{"class":205,"line":285},[203,21530,21531],{"class":219},"    Class\u003C",[203,21533,20064],{"class":209},[203,21535,20067],{"class":219},[203,21537,21538],{"class":215},"getObjectType",[203,21540,257],{"class":219},[203,21542,21543],{"class":205,"line":541},[203,21544,757],{"emptyLinePlaceholder":756},[203,21546,21547,21549,21552,21555],{"class":205,"line":549},[203,21548,19741],{"class":209},[203,21550,21551],{"class":209}," boolean",[203,21553,21554],{"class":215}," isSingleton",[203,21556,18242],{"class":219},[203,21558,21559,21561,21564],{"class":205,"line":787},[203,21560,18123],{"class":209},[203,21562,21563],{"class":718}," true",[203,21565,1297],{"class":219},[203,21567,21568],{"class":205,"line":799},[203,21569,751],{"class":219},[203,21571,21572],{"class":205,"line":1371},[203,21573,288],{"class":219},[18,21575,21576,21577,431],{},"假设 Bean 名称为 ",[22,21578,21579],{},"clientFactory",[144,21581,21584],{"className":21582,"code":21583,"language":149,"meta":150},[147],"getBean(\"clientFactory\")\n→ 获得 FactoryBean 生产的对象\n\ngetBean(\"&clientFactory\")\n→ 获得 FactoryBean 本身\n",[22,21585,21583],{"__ignoreMap":150},[18,21587,21588,21589,21592],{},"它适合创建代理、客户端、会话工厂等构造过程复杂的对象。MyBatis 与 Spring 集成中的 ",[22,21590,21591],{},"SqlSessionFactoryBean"," 就使用了这一机制。",[14,21594,21596],{"id":21595},"十七自定义-scope","十七、自定义 Scope",[18,21598,21599],{},"默认常见作用域包括：",[159,21601,21602,21607,21611],{},[162,21603,21604,2365],{},[22,21605,21606],{},"singleton",[162,21608,21609,2365],{},[22,21610,21318],{},[162,21612,21613,21614,53,21617,53,21620,53,21623,33],{},"Web 环境中的 ",[22,21615,21616],{},"request",[22,21618,21619],{},"session",[22,21621,21622],{},"application",[22,21624,21625],{},"websocket",[18,21627,21466,21628,21631,21632,21635],{},[22,21629,21630],{},"org.springframework.beans.factory.config.Scope"," 并通过 ",[22,21633,21634],{},"ConfigurableBeanFactory#registerScope()"," 注册，可以定义租户、任务、线程上下文等自定义作用域。",[18,21637,21638],{},"自定义 Scope 必须明确：",[159,21640,21641,21644,21647,21650,21653],{},[162,21642,21643],{},"实例保存在哪里；",[162,21645,21646],{},"何时创建和复用；",[162,21648,21649],{},"上下文结束时如何执行销毁回调；",[162,21651,21652],{},"上下文如何跨线程传播；",[162,21654,21655],{},"作用域 Bean 被单例依赖时是否需要代理。",[14,21657,21659],{"id":21658},"十八常见误区","十八、常见误区",[473,21661,21663],{"id":21662},"_1-bean-构造完成就可以使用","1. Bean 构造完成就可以使用",[18,21665,21666],{},"构造完成后还没有完成属性注入、Aware、初始化回调和后处理，不能把构造完成等同于 Bean 就绪。",[473,21668,21670],{"id":21669},"_2-postconstruct-发生在-aop-代理之后","2. @PostConstruct 发生在 AOP 代理之后",[18,21672,21673,21675],{},[22,21674,19253],{}," 属于初始化前处理阶段；AOP 代理通常在初始化后处理中形成并作为最终对象暴露。",[473,21677,21679],{"id":21678},"_3-beanpostprocessor-只在初始化前后各做一次简单回调","3. BeanPostProcessor 只在初始化前后各做一次简单回调",[18,21681,21682],{},"基础接口只有两个方法，但其子接口参与实例化前、构造器选择、属性填充、早期代理、合并定义和销毁等多个阶段。",[473,21684,21686],{"id":21685},"_4-beanfactorypostprocessor-可以直接修改业务-bean","4. BeanFactoryPostProcessor 可以直接修改业务 Bean",[18,21688,21689,21690,21692],{},"它应该修改 BeanDefinition。调用 ",[22,21691,19699],{}," 可能造成提前实例化并绕过其他基础设施。",[473,21694,21696],{"id":21695},"_5-bean-生命周期结束一定会执行销毁方法","5. Bean 生命周期结束一定会执行销毁方法",[18,21698,21699],{},"原型 Bean、进程强杀、机器掉电等场景都可能无法执行正常销毁回调。",[473,21701,21703],{"id":21702},"_6-所有初始化逻辑都放进-postconstruct","6. 所有初始化逻辑都放进 @PostConstruct",[18,21705,21706,21707,21709,21710,21712,21713,33],{},"Bean 自身的轻量校验可以放在 ",[22,21708,19253],{},"；依赖所有单例完成、容器刷新完成或需要启停管理的任务，应分别选择 ",[22,21711,19285],{},"、容器事件或 ",[22,21714,19293],{},[14,21716,21718],{"id":21717},"十九源码主线","十九、源码主线",[18,21720,21721],{},"阅读 Spring Bean 生命周期源码时，可以沿着以下调用链：",[144,21723,21726],{"className":21724,"code":21725,"language":149,"meta":150},[147],"AbstractApplicationContext#refresh\n├── invokeBeanFactoryPostProcessors\n├── registerBeanPostProcessors\n├── finishBeanFactoryInitialization\n│   └── preInstantiateSingletons\n│       └── getBean\n│           └── AbstractAutowireCapableBeanFactory#createBean\n│               ├── resolveBeforeInstantiation\n│               │   ├── applyBeanPostProcessorsBeforeInstantiation\n│               │   └── applyBeanPostProcessorsAfterInitialization\n│               │       （仅在实例化前返回非 null 对象时）\n│               └── doCreateBean\n│                   ├── createBeanInstance\n│                   ├── applyMergedBeanDefinitionPostProcessors\n│                   ├── addSingletonFactory\n│                   ├── populateBean\n│                   └── initializeBean\n│                       ├── invokeAwareMethods\n│                       ├── applyBeanPostProcessorsBeforeInitialization\n│                       ├── invokeInitMethods\n│                       └── applyBeanPostProcessorsAfterInitialization\n└── finishRefresh\n",[22,21727,21725],{"__ignoreMap":150},[18,21729,21730],{},"销毁主线可以关注：",[144,21732,21735],{"className":21733,"code":21734,"language":149,"meta":150},[147],"AbstractApplicationContext#close\n→ doClose\n→ LifecycleProcessor#onClose\n→ destroyBeans\n→ DisposableBeanAdapter#destroy\n",[22,21736,21734],{"__ignoreMap":150},[14,21738,21740],{"id":21739},"二十复习检查清单","二十、复习检查清单",[159,21742,21743,21746,21754,21757,21766,21769,21776,21779,21787,21790],{},[162,21744,21745],{},"能否区分容器生命周期和单个 Bean 生命周期；",[162,21747,21748,21749,6440,21751,21753],{},"能否说明 ",[22,21750,19235],{},[22,21752,19239],{}," 的区别；",[162,21755,21756],{},"是否知道依赖注入发生在实例化之后、初始化之前；",[162,21758,21759,21760,53,21762,21765],{},"能否说清 ",[22,21761,19253],{},[22,21763,21764],{},"InitializingBean","、自定义 init 方法的顺序；",[162,21767,21768],{},"是否知道 AOP 代理通常在哪个阶段创建；",[162,21770,21771,21772,21775],{},"能否解释三级缓存和 ",[22,21773,21774],{},"getEarlyBeanReference()"," 的作用；",[162,21777,21778],{},"是否知道单个 Bean 初始化完成后还有哪些容器级扩展点；",[162,21780,21748,21781,21783,21784,21786],{},[22,21782,19285],{},"、容器事件和 ",[22,21785,19293],{}," 的使用边界；",[162,21788,21789],{},"是否知道销毁回调的顺序以及原型 Bean 的限制；",[162,21791,21792],{},"能否按目标选择定义级、实例级、运行级和销毁级扩展点。",[14,21794,5803],{"id":5803},[159,21796,21797,21804,21811,21818,21825],{},[162,21798,21799],{},[119,21800,21803],{"href":21801,"rel":21802},"https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Freference\u002Fcore\u002Fbeans\u002Ffactory-extension.html",[2080],"Spring Framework：Container Extension Points",[162,21805,21806],{},[119,21807,21810],{"href":21808,"rel":21809},"https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Freference\u002Fcore\u002Fbeans\u002Ffactory-nature.html",[2080],"Spring Framework：Customizing the Nature of a Bean",[162,21812,21813],{},[119,21814,21817],{"href":21815,"rel":21816},"https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Freference\u002Fcore\u002Fbeans\u002Fbeanfactory.html",[2080],"Spring Framework：The BeanFactory API",[162,21819,21820],{},[119,21821,21824],{"href":21822,"rel":21823},"https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Fdocs\u002Fcurrent\u002Fjavadoc-api\u002Forg\u002Fspringframework\u002Fbeans\u002Ffactory\u002Fconfig\u002FInstantiationAwareBeanPostProcessor.html",[2080],"InstantiationAwareBeanPostProcessor API",[162,21826,21827],{},[119,21828,21831],{"href":21829,"rel":21830},"https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Fdocs\u002Fcurrent\u002Fjavadoc-api\u002Forg\u002Fspringframework\u002Fbeans\u002Ffactory\u002FSmartInitializingSingleton.html",[2080],"SmartInitializingSingleton API",[2111,21833,21834],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}",{"title":150,"searchDepth":251,"depth":251,"links":21836},[21837,21838,21839,21840,21844,21849,21852,21855,21862,21863,21864,21868,21869,21872,21873,21874,21879,21882,21883,21884,21885,21893,21894,21895],{"id":16,"depth":251,"text":16},{"id":115,"depth":251,"text":115},{"id":132,"depth":251,"text":132},{"id":19335,"depth":251,"text":19336,"children":21841},[21842,21843],{"id":19342,"depth":260,"text":19343},{"id":19355,"depth":260,"text":19356},{"id":19376,"depth":251,"text":19377,"children":21845},[21846,21847,21848],{"id":19440,"depth":260,"text":19441},{"id":19469,"depth":260,"text":19470},{"id":19494,"depth":260,"text":19495},{"id":19596,"depth":251,"text":19597,"children":21850},[21851],{"id":19600,"depth":260,"text":19235},{"id":19706,"depth":251,"text":19707,"children":21853},[21854],{"id":19859,"depth":260,"text":19859},{"id":19881,"depth":251,"text":19882,"children":21856},[21857,21858,21859,21860,21861],{"id":19892,"depth":260,"text":19893},{"id":19913,"depth":260,"text":19914},{"id":20311,"depth":260,"text":20312},{"id":20386,"depth":260,"text":20387},{"id":20420,"depth":260,"text":20421},{"id":20427,"depth":251,"text":20428},{"id":20465,"depth":251,"text":20466},{"id":20503,"depth":251,"text":20504,"children":21865},[21866,21867],{"id":20531,"depth":260,"text":20532},{"id":20547,"depth":260,"text":20548},{"id":20582,"depth":251,"text":20583},{"id":20689,"depth":251,"text":20690,"children":21870},[21871],{"id":20726,"depth":260,"text":20727},{"id":20747,"depth":251,"text":20748},{"id":20971,"depth":251,"text":20972},{"id":21032,"depth":251,"text":21033,"children":21875},[21876,21877,21878],{"id":21039,"depth":260,"text":21040},{"id":21099,"depth":260,"text":21100},{"id":21152,"depth":260,"text":21153},{"id":21249,"depth":251,"text":21250,"children":21880},[21881],{"id":21311,"depth":260,"text":21312},{"id":21331,"depth":251,"text":21332},{"id":21473,"depth":251,"text":21474},{"id":21595,"depth":251,"text":21596},{"id":21658,"depth":251,"text":21659,"children":21886},[21887,21888,21889,21890,21891,21892],{"id":21662,"depth":260,"text":21663},{"id":21669,"depth":260,"text":21670},{"id":21678,"depth":260,"text":21679},{"id":21685,"depth":260,"text":21686},{"id":21695,"depth":260,"text":21696},{"id":21702,"depth":260,"text":21703},{"id":21717,"depth":251,"text":21718},{"id":21739,"depth":251,"text":21740},{"id":5803,"depth":251,"text":5803},"wiki:java:spring-bean-lifecycle-and-extension-points","系统讲解 Spring 容器启动、Bean 定义处理、实例化、属性注入、初始化、AOP 代理、就绪与销毁的完整生命周期，并梳理 BeanFactoryPostProcessor、BeanPostProcessor、Aware、初始化回调、SmartInitializingSingleton、事件和 SmartLifecycle 等扩展点。",{},80,"\u002Fjava\u002Fspring-bean-lifecycle-and-extension-points",{"title":19209,"description":21897},"java\u002Fspring-bean-lifecycle-and-extension-points","ZtXQ8TCnwPnWeee0oKnQyksLUPgWGyb866r_87O5X4Q",{"id":21905,"title":21906,"body":21907,"commentId":23307,"description":23308,"difficulty":3667,"draft":2148,"extension":2149,"meta":23309,"navigation":756,"order":23310,"path":23311,"section":2153,"seo":23312,"stem":23313,"updated":23314,"__hash__":23315},"java\u002Fjava\u002Fjava-oom-troubleshooting.md","如果系统出现 OOM，应该如何处理？",{"type":7,"value":21908,"toc":23259},[21909,21912,21914,21917,21943,21946,21948,21955,21957,21966,21968,21972,21977,21980,22070,22077,22081,22084,22101,22104,22107,22111,22115,22118,22144,22148,22151,22157,22160,22163,22169,22172,22176,22179,22182,22188,22191,22197,22200,22204,22291,22294,22300,22303,22326,22334,22337,22341,22344,22348,22358,22362,22365,22371,22374,22380,22385,22389,22392,22398,22402,22406,22409,22435,22438,22441,22443,22466,22472,22486,22492,22495,22499,22502,22526,22529,22532,22535,22555,22558,22564,22568,22571,22574,22591,22601,22608,22612,22638,22642,22645,22648,22662,22665,22669,22672,22692,22694,22728,22731,22737,22740,22744,22747,22750,22772,22775,22795,22798,22802,22805,22838,22841,22868,22871,22875,22878,22895,22898,22902,22964,22967,22971,22974,22991,22994,23011,23014,23031,23034,23038,23041,23064,23067,23071,23075,23089,23092,23115,23123,23126,23143,23147,23151,23154,23158,23161,23165,23168,23172,23175,23179,23182,23186,23224,23226,23256],[10,21910,21906],{"id":21911},"如果系统出现-oom应该如何处理",[14,21913,16],{"id":16},[18,21915,21916],{},"系统发生 OOM 后，我会先止损并保留现场，而不是立即反复重启。首先确认影响范围，通过摘除故障实例、切换流量、限流或关闭非核心功能保护系统；同时保存 OOM 错误、应用日志、GC 日志、Heap Dump、容器事件和监控数据。证据保存完成后，再由进程托管系统重启或扩容恢复服务。",[18,21918,21919,21920,40,21923,21926,21927,21930,21931,21934,21935,21938,21939,21942],{},"然后根据错误类型确定排查方向。",[22,21921,21922],{},"Java heap space",[22,21924,21925],{},"GC overhead limit exceeded"," 重点分析堆；",[22,21928,21929],{},"Metaspace"," 重点检查类数量和 ClassLoader；",[22,21932,21933],{},"Direct buffer memory"," 重点检查直接内存和 Netty\u002FNIO Buffer；",[22,21936,21937],{},"unable to create native thread"," 重点检查线程数量、线程栈、系统限制和进程原生内存；容器出现 ",[22,21940,21941],{},"OOMKilled"," 时，还要检查进程 RSS、容器内存限制以及堆外内存，因为它不一定会留下 Java OOM。",[18,21944,21945],{},"堆内存问题通常使用 Heap Dump、对象直方图和 GC 日志分析。重点查看占用最大的对象、Retained Size、Dominator Tree 和到 GC Roots 的引用链，并结合关键 GC 周期后老年代基线是否持续增长，区分内存泄漏、正常容量不足和单次超大对象分配。使用 G1 时，应重点观察并发标记及随后一系列 Mixed GC 完成后的老年代占用。修复后需要用相同流量模型验证内存能否回落、GC 是否恢复正常，同时补齐自动 Dump、GC 日志、内存分区监控和容量告警。",[18,21947,103],{},[105,21949,21950],{},[18,21951,21952],{},[110,21953,21954],{},"OOM 处理的主线是先止损、再留证、按内存区域分类定位，最后区分泄漏、容量不足和异常大对象，并通过同等流量验证修复。",[14,21956,115],{"id":115},[18,21958,21959],{},[119,21960,21963],{"href":21961,"target":122,"rel":21962},"\u002Fimages\u002Fwiki\u002Fjava\u002Fjava-oom-troubleshooting.svg",[124,125],[127,21964],{"src":21961,"alt":21965},"Java OOM 止损、留证、分类定位、修复验证与预防流程",[14,21967,132],{"id":132},[14,21969,21971],{"id":21970},"一oom-不等于-java-堆内存泄漏","一、OOM 不等于 Java 堆内存泄漏",[18,21973,21974,21976],{},[22,21975,52],{}," 表示 JVM 无法满足某次资源申请，但发生问题的资源不一定是 Java 堆，也不一定存在泄漏。",[18,21978,21979],{},"常见类型如下：",[299,21981,21982,21992],{},[302,21983,21984],{},[305,21985,21986,21989],{},[308,21987,21988],{},"错误信息",[308,21990,21991],{},"主要排查方向",[315,21993,21994,22003,22012,22021,22031,22040,22049,22059],{},[305,21995,21996,22000],{},[320,21997,21998],{},[22,21999,21922],{},[320,22001,22002],{},"堆容量不足、对象泄漏、瞬时大对象",[305,22004,22005,22009],{},[320,22006,22007],{},[22,22008,21925],{},[320,22010,22011],{},"堆长期接近耗尽，GC 频繁但回收效果很差",[305,22013,22014,22018],{},[320,22015,22016],{},[22,22017,21929],{},[320,22019,22020],{},"类元数据过多、ClassLoader 泄漏、动态生成类失控",[305,22022,22023,22028],{},[320,22024,22025],{},[22,22026,22027],{},"Compressed class space",[320,22029,22030],{},"压缩类指针空间不足",[305,22032,22033,22037],{},[320,22034,22035],{},[22,22036,21933],{},[320,22038,22039],{},"NIO、Netty 等直接内存申请过多或释放不及时",[305,22041,22042,22046],{},[320,22043,22044],{},[22,22045,21937],{},[320,22047,22048],{},"线程过多、线程栈占用过大、系统线程限制或原生内存不足",[305,22050,22051,22056],{},[320,22052,22053],{},[22,22054,22055],{},"Requested array size exceeds VM limit",[320,22057,22058],{},"代码尝试创建超过 JVM 限制的超大数组",[305,22060,22061,22067],{},[320,22062,22063,22064,22066],{},"容器 ",[22,22065,21941],{},"、退出码 137",[320,22068,22069],{},"进程总内存超过容器限制，不一定出现 Java OOM",[18,22071,22072,22073,22076],{},"因此，第一步不是直接调大 ",[22,22074,22075],{},"-Xmx","，而是先确认错误类型和耗尽的内存区域。",[14,22078,22080],{"id":22079},"二第一阶段先止损","二、第一阶段：先止损",[18,22082,22083],{},"线上故障首先要控制影响范围：",[2695,22085,22086,22089,22092,22095,22098],{},[162,22087,22088],{},"从负载均衡中摘除异常实例，避免继续接收新流量；",[162,22090,22091],{},"有健康副本时切换流量或临时扩容；",[162,22093,22094],{},"对非核心接口限流、熔断或降级；",[162,22096,22097],{},"暂停可能持续放大内存的批处理、消费任务或大查询；",[162,22099,22100],{},"确认现场信息已经保存后，再重启异常实例。",[18,22102,22103],{},"是否立即重启取决于可用性和取证条件。如果服务已经不可用，并且有自动生成的 Heap Dump、GC 日志和监控数据，可以尽快重启恢复；如果没有任何证据且仍能安全操作，应先保留必要现场。",[18,22105,22106],{},"不要让已经发生严重 OOM 的进程长期勉强提供服务。此时进程可能持续 Full GC、请求超时，甚至无法创建用于自救的新线程。",[14,22108,22110],{"id":22109},"三第二阶段保留现场","三、第二阶段：保留现场",[473,22112,22114],{"id":22113},"_1-记录基本信息","1. 记录基本信息",[18,22116,22117],{},"至少保存：",[159,22119,22120,22123,22126,22129,22132,22135,22138,22141],{},[162,22121,22122],{},"完整 OOM 错误和调用栈；",[162,22124,22125],{},"故障时间、实例、版本和流量变化；",[162,22127,22128],{},"JVM 启动参数；",[162,22130,22131],{},"GC 日志；",[162,22133,22134],{},"Heap Dump；",[162,22136,22137],{},"堆、元空间、直接内存、线程数和进程 RSS 监控；",[162,22139,22140],{},"容器事件、退出原因和内存限制；",[162,22142,22143],{},"故障前后的发布、配置和任务变更。",[473,22145,22147],{"id":22146},"_2-提前配置自动-heap-dump","2. 提前配置自动 Heap Dump",[18,22149,22150],{},"生产环境应在启动时配置：",[144,22152,22155],{"className":22153,"code":22154,"language":149,"meta":150},[147],"-XX:+HeapDumpOnOutOfMemoryError\n-XX:HeapDumpPath=\u002Fdata\u002Fdumps\n",[22,22156,22154],{"__ignoreMap":150},[18,22158,22159],{},"Dump 目录必须有足够空间和写权限，并有清理策略。Heap Dump 可能包含用户数据、令牌或业务字段，下载和保存时要遵守数据安全要求。",[18,22161,22162],{},"对于由进程托管系统负责自动拉起的无状态服务，可以评估：",[144,22164,22167],{"className":22165,"code":22166,"language":149,"meta":150},[147],"-XX:+ExitOnOutOfMemoryError\n",[22,22168,22166],{"__ignoreMap":150},[18,22170,22171],{},"它让 JVM 在 OOM 后退出，由 Kubernetes、systemd 等外部系统重启。是否使用取决于服务容灾能力，不能替代现场保留和根因修复。",[473,22173,22175],{"id":22174},"_3-gc-日志由谁生成","3. GC 日志由谁生成",[18,22177,22178],{},"GC 日志由 JVM 生成，不需要在业务代码中手动打印，但必须通过 JVM 启动参数提前开启。应用使用的 Logback、Log4j 等日志框架不会自动记录完整 GC 日志。",[18,22180,22181],{},"JDK 9 及以上可以使用统一日志参数：",[144,22183,22186],{"className":22184,"code":22185,"language":149,"meta":150},[147],"-Xlog:gc*:file=\u002Fdata\u002Flogs\u002Fgc.log:time,uptime,level,tags:filecount=10,filesize=100M\n",[22,22187,22185],{"__ignoreMap":150},[18,22189,22190],{},"JDK 8 常见配置为：",[144,22192,22195],{"className":22193,"code":22194,"language":149,"meta":150},[147],"-XX:+PrintGCDetails\n-XX:+PrintGCDateStamps\n-Xloggc:\u002Fdata\u002Flogs\u002Fgc.log\n",[22,22196,22194],{"__ignoreMap":150},[18,22198,22199],{},"如果故障发生前没有开启，就无法在事后补回完整的历史 GC 日志，只能结合监控、Heap Dump、JFR 或其他现场信息分析。",[473,22201,22203],{"id":22202},"_4-常用诊断命令","4. 常用诊断命令",[144,22205,22207],{"className":7980,"code":22206,"language":7982,"meta":150,"style":150},"jcmd \u003Cpid> VM.flags\njcmd \u003Cpid> GC.heap_info\njcmd \u003Cpid> GC.class_histogram\njcmd \u003Cpid> Thread.print\njcmd \u003Cpid> GC.heap_dump \u002Fdata\u002Fdumps\u002Fapp.hprof\n",[22,22208,22209,22228,22243,22258,22273],{"__ignoreMap":150},[203,22210,22211,22214,22217,22220,22223,22225],{"class":205,"line":206},[203,22212,22213],{"class":215},"jcmd",[203,22215,22216],{"class":209}," \u003C",[203,22218,22219],{"class":223},"pi",[203,22221,22222],{"class":219},"d",[203,22224,9371],{"class":209},[203,22226,22227],{"class":223}," VM.flags\n",[203,22229,22230,22232,22234,22236,22238,22240],{"class":205,"line":251},[203,22231,22213],{"class":215},[203,22233,22216],{"class":209},[203,22235,22219],{"class":223},[203,22237,22222],{"class":219},[203,22239,9371],{"class":209},[203,22241,22242],{"class":223}," GC.heap_info\n",[203,22244,22245,22247,22249,22251,22253,22255],{"class":205,"line":260},[203,22246,22213],{"class":215},[203,22248,22216],{"class":209},[203,22250,22219],{"class":223},[203,22252,22222],{"class":219},[203,22254,9371],{"class":209},[203,22256,22257],{"class":223}," GC.class_histogram\n",[203,22259,22260,22262,22264,22266,22268,22270],{"class":205,"line":278},[203,22261,22213],{"class":215},[203,22263,22216],{"class":209},[203,22265,22219],{"class":223},[203,22267,22222],{"class":219},[203,22269,9371],{"class":209},[203,22271,22272],{"class":223}," Thread.print\n",[203,22274,22275,22277,22279,22281,22283,22285,22288],{"class":205,"line":285},[203,22276,22213],{"class":215},[203,22278,22216],{"class":209},[203,22280,22219],{"class":223},[203,22282,22222],{"class":219},[203,22284,9371],{"class":209},[203,22286,22287],{"class":223}," GC.heap_dump",[203,22289,22290],{"class":223}," \u002Fdata\u002Fdumps\u002Fapp.hprof\n",[18,22292,22293],{},"原生内存跟踪需要在 JVM 启动时开启：",[144,22295,22298],{"className":22296,"code":22297,"language":149,"meta":150},[147],"-XX:NativeMemoryTracking=summary\n",[22,22299,22297],{"__ignoreMap":150},[18,22301,22302],{},"开启后可以查看：",[144,22304,22306],{"className":7980,"code":22305,"language":7982,"meta":150,"style":150},"jcmd \u003Cpid> VM.native_memory summary\n",[22,22307,22308],{"__ignoreMap":150},[203,22309,22310,22312,22314,22316,22318,22320,22323],{"class":205,"line":206},[203,22311,22213],{"class":215},[203,22313,22216],{"class":209},[203,22315,22219],{"class":223},[203,22317,22222],{"class":219},[203,22319,9371],{"class":209},[203,22321,22322],{"class":223}," VM.native_memory",[203,22324,22325],{"class":223}," summary\n",[105,22327,22328],{},[18,22329,22330,22333],{},[110,22331,22332],{},"NMT（Native Memory Tracking）"," 是 HotSpot 提供的原生内存跟踪能力，用于统计 JVM 在堆外为类元数据、线程、代码缓存、GC 等模块申请的原生内存。它需要在启动时开启，且不能覆盖所有第三方原生库的内存分配。",[18,22335,22336],{},"在线执行对象直方图或 Heap Dump 可能引起停顿和大量磁盘 I\u002FO。操作前要确认磁盘空间、实例影响和可用副本，优先使用 OOM 时自动生成的 Dump。",[14,22338,22340],{"id":22339},"四先判断是-jvm-oom还是容器-oomkilled","四、先判断是 JVM OOM，还是容器 OOMKilled",[18,22342,22343],{},"这两类问题经常被混淆。",[473,22345,22347],{"id":22346},"jvm-oom","JVM OOM",[18,22349,22350,22351,22353,22354,22357],{},"JVM 自己发现某个资源无法继续分配，通常会在日志中出现具体的 ",[22,22352,52],{},"。如果配置了自动 Dump，可能生成 ",[22,22355,22356],{},".hprof"," 文件。",[473,22359,22361],{"id":22360},"容器-oomkilled","容器 OOMKilled",[18,22363,22364],{},"当进程总内存超过容器的 cgroup 限制时，操作系统可能直接杀掉进程。Kubernetes 中常见现象是：",[144,22366,22369],{"className":22367,"code":22368,"language":149,"meta":150},[147],"Reason: OOMKilled\nExit Code: 137\n",[22,22370,22368],{"__ignoreMap":150},[18,22372,22373],{},"这时进程可能来不及抛出 Java OOM，也可能没有 Heap Dump。容器计算的是进程总内存，不只是 Java 堆：",[144,22375,22378],{"className":22376,"code":22377,"language":149,"meta":150},[147],"进程总内存\n≈ Java Heap\n+ Metaspace\n+ Direct Memory\n+ 线程栈\n+ Code Cache\n+ JVM 原生结构\n+ 共享库和其他映射\n",[22,22379,22377],{"__ignoreMap":150},[18,22381,4262,22382,22384],{},[22,22383,22075],{}," 几乎等于容器内存上限，即使堆没有耗尽，也可能因为堆外空间、线程栈或 JVM 自身开销触发 OOMKilled。",[14,22386,22388],{"id":22387},"五java-heap-space-如何分析","五、Java Heap Space 如何分析",[18,22390,22391],{},"堆 OOM 通常要回答三个问题：",[144,22393,22396],{"className":22394,"code":22395,"language":149,"meta":150},[147],"谁占用了内存？\n为什么没有被回收？\n占用是持续增长，还是一次性突增？\n",[22,22397,22395],{"__ignoreMap":150},[473,22399,22401],{"id":22400},"_1-按垃圾收集器分析-gc-趋势","1. 按垃圾收集器分析 GC 趋势",[6462,22403,22405],{"id":22404},"g1重点观察并发标记和-mixed-gc","G1：重点观察并发标记和 Mixed GC",[18,22407,22408],{},"G1 的正常回收链路不是简单地等待一次 Full GC，而是在年轻代回收之外，周期性地完成老年代存活分析和增量空间回收：",[2695,22410,22411,22417,22423,22429],{},[162,22412,22413,22416],{},[110,22414,22415],{},"并发标记","：统计对象的存活情况，为判断哪些老年代 Region 具有回收价值提供依据；",[162,22418,22419,22422],{},[110,22420,22421],{},"Remark","：在 Stop-The-World 停顿中完成标记，处理引用和类卸载等工作，并根据存活数据确定老年代候选 Region；",[162,22424,22425,22428],{},[110,22426,22427],{},"Cleanup","：完成候选 Region 的整理和排序，并决定是否进入空间回收阶段；",[162,22430,22431,22434],{},[110,22432,22433],{},"Mixed GC","：每次都回收本次年轻代 Region，同时从候选集合中选择部分老年代 Region 一起回收。",[18,22436,22437],{},"因此，Mixed GC 的 Collection Set 同时包含年轻代 Region 和部分老年代候选 Region。“老年代候选 Region”是并发标记、Remark 和 Cleanup 这条链路产生的；普通年轻代 Region 不需要由 Remark 从中筛选，而是在每次 Young GC 或 Mixed GC 构建 Collection Set 时按年轻代回收规则加入。",[18,22439,22440],{},"分析 G1 时，应观察一轮完整的并发标记及后续一系列 Mixed GC，而不是只看某一次 Young GC。",[18,22442,13770],{},[159,22444,22445,22448,22451,22454,22457,22460,22463],{},[162,22446,22447],{},"Young GC 是否越来越频繁、停顿时间是否持续增长；",[162,22449,22450],{},"并发标记周期是否能够及时启动并正常完成；",[162,22452,22453],{},"一系列 Mixed GC 完成后，老年代 Region 占用能否明显回落；",[162,22455,22456],{},"老年代回收后的内存基线是否持续上升；",[162,22458,22459],{},"Humongous Region 是否持续增加；",[162,22461,22462],{},"GC 日志中是否出现 Evacuation Failure 或 To-space exhausted；",[162,22464,22465],{},"空间持续不足后是否触发 Full GC。",[18,22467,22468,22471],{},[22,22469,22470],{},"Evacuation Failure"," 表示 G1 在回收期间未能把部分存活对象迁移到目标 Region。常见原因包括：",[159,22473,22474,22480],{},[162,22475,22476,22479],{},[22,22477,22478],{},"Allocation","：找不到足够的目标空间容纳待迁移对象；",[162,22481,22482,22485],{},[22,22483,22484],{},"Pinned","：对象因 JNI 等原因被固定，暂时不能移动。",[18,22487,22488,22491],{},[22,22489,22490],{},"To-space exhausted"," 是较早版本 GC 日志中的常见描述，表示用于接收存活对象的空闲 Region 不足，可以理解为 Evacuation Failure 的一种空间不足表现。",[18,22493,22494],{},"出现一次 Evacuation Failure 并不代表 G1 会立即执行 Full GC。G1 可以保留未完成迁移的 Region，并在后续 GC 中继续处理；如果堆空间持续紧张，GC 最终仍无法释放足够空间，G1 才会调度一次 Stop-The-World 的 Full GC，对整个堆进行压缩。因此更准确的说法是“空间持续不足后触发 Full GC”，而不是“G1 退化为 Full GC”。",[6462,22496,22498],{"id":22497},"cms重点观察并发周期和老年代回收结果","CMS：重点观察并发周期和老年代回收结果",[18,22500,22501],{},"CMS 主要通过并发标记和并发清除回收老年代，排查时重点观察：",[159,22503,22504,22507,22510,22517,22523],{},[162,22505,22506],{},"CMS 周期是否越来越频繁；",[162,22508,22509],{},"一轮 CMS 周期完成后，老年代占用能否明显下降；",[162,22511,22512,22513,22516],{},"是否出现 ",[22,22514,22515],{},"concurrent mode failure","，即 CMS 尚未完成，老年代已经没有足够空间；",[162,22518,22512,22519,22522],{},[22,22520,22521],{},"promotion failed","，即新生代对象晋升时老年代无法提供足够空间；",[162,22524,22525],{},"老年代碎片是否持续增加，并最终触发代价较高的 Full GC。",[18,22527,22528],{},"CMS 不在正常并发回收过程中整理存活对象，因此即使老年代总剩余空间看起来尚可，也可能因缺少足够大的连续空间而发生晋升失败或 Full GC。",[6462,22530,22531],{"id":22531},"通用判断方法",[18,22533,22534],{},"无论使用哪种收集器，都需要结合以下趋势判断：",[159,22536,22537,22540,22543,22546,22549,22552],{},[162,22538,22539],{},"老年代使用量是否持续增长；",[162,22541,22542],{},"关键 GC 周期是否越来越频繁；",[162,22544,22545],{},"关键 GC 周期后内存能否明显下降；",[162,22547,22548],{},"对象分配速率是否突然升高；",[162,22550,22551],{},"晋升速率和大对象分配是否异常；",[162,22553,22554],{},"GC 停顿是否已经影响业务。",[18,22556,22557],{},"其中，“关键回收周期”在 G1 下主要指一轮并发标记及后续一系列 Mixed GC，在 CMS 下主要指一轮完整的 CMS 周期：",[144,22559,22562],{"className":22560,"code":22561,"language":149,"meta":150},[147],"关键回收周期后老年代基线持续上升\n→ 更像对象泄漏或无上限缓存\n\n回收后能够下降，但高峰期仍然耗尽\n→ 更像容量不足或瞬时流量过大\n\n内存突然被单次请求快速打满\n→ 检查超大数组、文件、结果集或批次\n",[22,22563,22561],{"__ignoreMap":150},[473,22565,22567],{"id":22566},"_2-分析-heap-dump","2. 分析 Heap Dump",[18,22569,22570],{},"常用工具包括 Eclipse MAT、JDK Mission Control 和 VisualVM。",[18,22572,22573],{},"重点关注：",[159,22575,22576,22579,22582,22585,22588],{},[162,22577,22578],{},"Histogram：各类对象的实例数和浅堆大小；",[162,22580,22581],{},"Dominator Tree：哪些对象支配了大量内存；",[162,22583,22584],{},"Retained Size：删除某对象后理论上能够释放多少内存；",[162,22586,22587],{},"Path to GC Roots：对象为什么仍然可达；",[162,22589,22590],{},"Leak Suspects：工具给出的泄漏候选，需要结合业务验证。",[18,22592,22593,22596,22597,22600],{},[22,22594,22595],{},"Shallow Size"," 只表示对象自身大小；",[22,22598,22599],{},"Retained Size"," 还包括只能通过该对象访问的其他对象。排查泄漏时，Retained Size 通常更有价值。",[105,22602,22603],{},[18,22604,22605,22607],{},[110,22606,22599],{}," 表示：如果某个对象被回收，并且没有其他引用路径继续持有其支配的对象，理论上能够一并释放的堆内存总量。它用于衡量对象实际“留住”了多少内存，不等于对象自身大小，也不代表这些内存会立刻返还给操作系统。",[473,22609,22611],{"id":22610},"_3-常见堆泄漏来源","3. 常见堆泄漏来源",[159,22613,22614,22617,22620,22623,22626,22629,22632,22635],{},[162,22615,22616],{},"没有容量上限或过期机制的本地缓存；",[162,22618,22619],{},"静态集合持续保存业务对象；",[162,22621,22622],{},"ThreadLocal 使用后没有清理；",[162,22624,22625],{},"监听器、回调或订阅关系只注册不注销；",[162,22627,22628],{},"队列无界，生产速度长期高于消费速度；",[162,22630,22631],{},"一次性加载超大文件、结果集或消息批次；",[162,22633,22634],{},"ORM 上下文、会话对象长期持有实体；",[162,22636,22637],{},"重试任务、Future 或上下文对象长期积压。",[14,22639,22641],{"id":22640},"六gc-overhead-limit-exceeded-是什么","六、GC Overhead Limit Exceeded 是什么",[18,22643,22644],{},"这个错误表示 JVM 长时间忙于执行 GC，但每次只能回收很少的堆空间。它描述的是“GC 开销极高、回收收益极低”的状态，通常说明 Java 堆压力已经非常严重，而不是指某个新的内存区域发生了 OOM。",[18,22646,22647],{},"排查方法与堆 OOM 基本一致：",[2695,22649,22650,22653,22656,22659],{},[162,22651,22652],{},"查看 GC 频率以及关键回收周期前后的堆占用；",[162,22654,22655],{},"分析 Heap Dump 中的主要存活对象；",[162,22657,22658],{},"检查是否存在内存泄漏或缓存、队列无上限；",[162,22660,22661],{},"检查堆容量是否与真实负载匹配。",[18,22663,22664],{},"只关闭 GC overhead 检查通常没有解决根因，只会让进程以另一种方式继续恶化。",[14,22666,22668],{"id":22667},"七metaspace-oom-如何分析","七、Metaspace OOM 如何分析",[18,22670,22671],{},"Metaspace 保存类元数据。发生 OOM 时重点检查：",[159,22673,22674,22677,22680,22683,22686],{},[162,22675,22676],{},"已加载类数量是否持续增长；",[162,22678,22679],{},"是否频繁动态生成代理类、脚本类或字节码增强类；",[162,22681,22682],{},"是否存在大量无法卸载的 ClassLoader；",[162,22684,22685],{},"热部署、插件化或脚本引擎是否反复创建类加载器；",[162,22687,22688,22691],{},[22,22689,22690],{},"-XX:MaxMetaspaceSize"," 是否设置得过小。",[18,22693,21108],{},[144,22695,22697],{"className":7980,"code":22696,"language":7982,"meta":150,"style":150},"jcmd \u003Cpid> VM.classloader_stats\njcmd \u003Cpid> GC.class_histogram\n",[22,22698,22699,22714],{"__ignoreMap":150},[203,22700,22701,22703,22705,22707,22709,22711],{"class":205,"line":206},[203,22702,22213],{"class":215},[203,22704,22216],{"class":209},[203,22706,22219],{"class":223},[203,22708,22222],{"class":219},[203,22710,9371],{"class":209},[203,22712,22713],{"class":223}," VM.classloader_stats\n",[203,22715,22716,22718,22720,22722,22724,22726],{"class":205,"line":251},[203,22717,22213],{"class":215},[203,22719,22216],{"class":209},[203,22721,22219],{"class":223},[203,22723,22222],{"class":219},[203,22725,9371],{"class":209},[203,22727,22257],{"class":223},[18,22729,22730],{},"典型泄漏链路是：",[144,22732,22735],{"className":22733,"code":22734,"language":149,"meta":150},[147],"业务线程、ThreadLocal、静态对象或第三方库\n→ 持有自定义 ClassLoader\n→ ClassLoader 持有其加载的全部 Class\n→ 类元数据无法卸载\n",[22,22736,22734],{"__ignoreMap":150},[18,22738,22739],{},"简单调大 Metaspace 只能延迟故障。类数量持续增长时，应定位是哪个 ClassLoader 和动态类生成逻辑没有释放。",[14,22741,22743],{"id":22742},"八direct-buffer-memory-如何分析","八、Direct Buffer Memory 如何分析",[18,22745,22746],{},"直接内存位于 Java 堆之外，常见使用者包括 NIO、Netty、压缩库和网络框架。",[18,22748,22749],{},"重点检查：",[159,22751,22752,22755,22760,22763,22766,22769],{},[162,22753,22754],{},"DirectByteBuffer 数量和容量；",[162,22756,22757,2365],{},[22,22758,22759],{},"-XX:MaxDirectMemorySize",[162,22761,22762],{},"Netty 池化内存和引用计数是否正确释放；",[162,22764,22765],{},"是否缓存了大量直接缓冲区；",[162,22767,22768],{},"是否存在大文件或网络数据的堆外缓冲；",[162,22770,22771],{},"进程 RSS 是否增长，但 Java 堆保持稳定。",[18,22773,22774],{},"如果启用了 NMT，可以通过：",[144,22776,22777],{"className":7980,"code":22305,"language":7982,"meta":150,"style":150},[22,22778,22779],{"__ignoreMap":150},[203,22780,22781,22783,22785,22787,22789,22791,22793],{"class":205,"line":206},[203,22782,22213],{"class":215},[203,22784,22216],{"class":209},[203,22786,22219],{"class":223},[203,22788,22222],{"class":219},[203,22790,9371],{"class":209},[203,22792,22322],{"class":223},[203,22794,22325],{"class":223},[18,22796,22797],{},"查看 JVM 能追踪到的原生内存分类。需要注意，NMT 不能解释所有第三方原生库分配，仍要结合框架指标、进程 RSS 和操作系统工具分析。",[14,22799,22801],{"id":22800},"九unable-to-create-native-thread-如何分析","九、Unable to Create Native Thread 如何分析",[18,22803,22804],{},"创建线程不仅需要 Java 对象，还需要原生线程和线程栈。常见原因包括：",[159,22806,22807,22810,22816,22823,22826,22832,22835],{},[162,22808,22809],{},"线程池配置失控或不断创建新线程；",[162,22811,4104,22812,22815],{},[22,22813,22814],{},"newCachedThreadPool()"," 等高上限线程池承接阻塞任务；",[162,22817,22818,22819,22822],{},"每次处理请求或任务时都重新执行 ",[22,22820,22821],{},"new Thread()","，没有复用受控线程池；",[162,22824,22825],{},"线程阻塞、死锁或下游超时，旧线程迟迟不能释放；",[162,22827,22828,22831],{},[22,22829,22830],{},"-Xss"," 设置过大；",[162,22833,22834],{},"容器原生内存不足；",[162,22836,22837],{},"操作系统或容器的 PID、用户线程数限制。",[18,22839,22840],{},"排查时可以结合：",[144,22842,22844],{"className":7980,"code":22843,"language":7982,"meta":150,"style":150},"jcmd \u003Cpid> Thread.print\nps -eLf\n",[22,22845,22846,22860],{"__ignoreMap":150},[203,22847,22848,22850,22852,22854,22856,22858],{"class":205,"line":206},[203,22849,22213],{"class":215},[203,22851,22216],{"class":209},[203,22853,22219],{"class":223},[203,22855,22222],{"class":219},[203,22857,9371],{"class":209},[203,22859,22272],{"class":223},[203,22861,22862,22865],{"class":205,"line":251},[203,22863,22864],{"class":215},"ps",[203,22866,22867],{"class":718}," -eLf\n",[18,22869,22870],{},"重点统计线程总数、线程名称分布和线程状态，确认是哪一个线程池或业务模块持续增长。解决方向通常是限制线程数量、使用有界队列、修复阻塞和超时、释放线程资源，而不是只调整系统线程上限。",[14,22872,22874],{"id":22873},"十requested-array-size-exceeds-vm-limit","十、Requested Array Size Exceeds VM Limit",[18,22876,22877],{},"该错误通常说明代码尝试创建一个超过 JVM 可表示或可管理范围的数组，例如：",[159,22879,22880,22883,22886,22889,22892],{},[162,22881,22882],{},"根据错误长度计算数组容量；",[162,22884,22885],{},"整数溢出后得到异常容量；",[162,22887,22888],{},"一次性把超大文件读入 byte 数组；",[162,22890,22891],{},"一次性拼接超大字符串；",[162,22893,22894],{},"查询结果或批处理缺少分页。",[18,22896,22897],{},"这类问题的重点是定位申请数组的调用栈并修改数据处理方式，例如分页、流式读取、分块处理或限制单次请求大小。单纯增加堆通常无效。",[14,22899,22901],{"id":22900},"十一如何区分泄漏容量不足和异常大对象","十一、如何区分泄漏、容量不足和异常大对象",[299,22903,22904,22914],{},[302,22905,22906],{},[305,22907,22908,22911],{},[308,22909,22910],{},"现象",[308,22912,22913],{},"更可能的原因",[315,22915,22916,22924,22932,22940,22948,22956],{},[305,22917,22918,22921],{},[320,22919,22920],{},"关键 GC 周期后老年代基线仍持续上升",[320,22922,22923],{},"对象泄漏、无上限缓存或队列",[305,22925,22926,22929],{},[320,22927,22928],{},"内存随流量上升，流量下降后能够回落",[320,22930,22931],{},"正常业务占用或容量不足",[305,22933,22934,22937],{},[320,22935,22936],{},"某次请求后内存瞬间大幅增长",[320,22938,22939],{},"超大对象、文件、结果集或批次",[305,22941,22942,22945],{},[320,22943,22944],{},"Java 堆稳定，但进程 RSS 持续增长",[320,22946,22947],{},"直接内存、线程栈或原生库",[305,22949,22950,22953],{},[320,22951,22952],{},"类数量和 Metaspace 持续增长",[320,22954,22955],{},"ClassLoader 或动态类泄漏",[305,22957,22958,22961],{},[320,22959,22960],{},"线程数持续增长并最终创建失败",[320,22962,22963],{},"线程泄漏、阻塞或线程池失控",[18,22965,22966],{},"判断不能只靠故障时的一张截图。应把 Heap Dump、GC 日志、时间序列监控、流量和发布记录结合起来。",[14,22968,22970],{"id":22969},"十二修复时应该做什么","十二、修复时应该做什么",[473,22972,22973],{"id":22973},"内存泄漏",[159,22975,22976,22979,22982,22985,22988],{},[162,22977,22978],{},"删除无效引用；",[162,22980,22981],{},"给缓存和集合增加容量、过期和清理策略；",[162,22983,22984],{},"正确清理 ThreadLocal、监听器和回调；",[162,22986,22987],{},"修复 ClassLoader、Buffer 或原生资源释放；",[162,22989,22990],{},"为队列和重试任务设置上限。",[473,22992,22993],{"id":22993},"容量不足",[159,22995,22996,22999,23002,23005,23008],{},[162,22997,22998],{},"根据峰值存活对象和分配速率重新评估堆容量；",[162,23000,23001],{},"优化对象生命周期和数据结构；",[162,23003,23004],{},"降低单次批量大小；",[162,23006,23007],{},"扩容实例并重新分配流量；",[162,23009,23010],{},"调整容器上限时为堆外内存保留空间。",[473,23012,23013],{"id":23013},"异常大对象或突发流量",[159,23015,23016,23019,23022,23025,23028],{},[162,23017,23018],{},"限制请求体、文件、分页和批次大小；",[162,23020,23021],{},"使用流式处理代替一次性加载；",[162,23023,23024],{},"对大查询增加保护；",[162,23026,23027],{},"在入口处限流和拒绝异常请求；",[162,23029,23030],{},"避免把外部积压无限转移到 JVM 内存。",[18,23032,23033],{},"调大内存可以作为临时止损手段，但必须建立在容量评估之上。存在泄漏时，内存越大通常只是让故障更晚发生。",[14,23035,23037],{"id":23036},"十三如何验证修复有效","十三、如何验证修复有效",[18,23039,23040],{},"修复后不能只确认“暂时没有再 OOM”，还要在接近真实的流量模型下验证：",[159,23042,23043,23046,23049,23052,23055,23058,23061],{},[162,23044,23045],{},"老年代在 GC 后能够回落并保持稳定；",[162,23047,23048],{},"对象数量不再无界增长；",[162,23050,23051],{},"Metaspace、直接内存和线程数保持在合理范围；",[162,23053,23054],{},"GC 频率、停顿和吞吐符合目标；",[162,23056,23057],{},"进程 RSS 与容器限制之间有足够余量；",[162,23059,23060],{},"高峰、降峰和长时间运行都没有持续爬升；",[162,23062,23063],{},"限流、降级和自动重启策略能够正确触发。",[18,23065,23066],{},"内存泄漏往往需要时间积累，短时间功能测试不能代替持续稳定性验证。",[14,23068,23070],{"id":23069},"十四上线前应该建立哪些预防能力","十四、上线前应该建立哪些预防能力",[473,23072,23074],{"id":23073},"jvm-证据","JVM 证据",[159,23076,23077,23080,23083,23086],{},[162,23078,23079],{},"开启 OOM 自动 Heap Dump；",[162,23081,23082],{},"保存并轮转 GC 日志；",[162,23084,23085],{},"记录 JVM 参数和应用版本；",[162,23087,23088],{},"在需要分析原生内存的服务上评估开启 NMT。",[473,23090,23091],{"id":23091},"监控告警",[159,23093,23094,23097,23100,23103,23106,23109,23112],{},[162,23095,23096],{},"Heap 各代使用量及 GC 后基线；",[162,23098,23099],{},"GC 次数、停顿和回收量；",[162,23101,23102],{},"Metaspace 和已加载类数量；",[162,23104,23105],{},"Direct Buffer 使用量；",[162,23107,23108],{},"线程总数和各线程池状态；",[162,23110,23111],{},"进程 RSS、容器 Working Set 和 OOMKilled；",[162,23113,23114],{},"队列长度、缓存数量、请求体和批次大小。",[105,23116,23117],{},[18,23118,23119,23122],{},[110,23120,23121],{},"容器 Working Set"," 通常近似表示 cgroup 内存使用量扣除不活跃文件缓存后的部分，用于衡量当前不容易被操作系统立即回收的内存。它不是 Java Heap，也不完全等同于单个 Java 进程的 RSS，排查时需要结合两者一起看。",[473,23124,23125],{"id":23125},"容量与保护",[159,23127,23128,23131,23134,23137,23140],{},[162,23129,23130],{},"为容器总内存预留堆外空间；",[162,23132,23133],{},"缓存、队列、线程池和批次必须有明确上限；",[162,23135,23136],{},"大请求、大文件和大查询需要入口保护；",[162,23138,23139],{},"关键服务具备摘流、限流、降级和快速重启能力；",[162,23141,23142],{},"定期进行内存压测和故障演练。",[14,23144,23146],{"id":23145},"十五常见错误做法","十五、常见错误做法",[473,23148,23150],{"id":23149},"_1-一出现-oom-就把-xmx-调大","1. 一出现 OOM 就把 Xmx 调大",[18,23152,23153],{},"这可能暂时缓解容量不足，但无法修复泄漏、线程失控、直接内存或 ClassLoader 问题。",[473,23155,23157],{"id":23156},"_2-重启后不保留任何证据","2. 重启后不保留任何证据",[18,23159,23160],{},"重启会清空最重要的现场。至少应提前配置自动 Dump、GC 日志和监控。",[473,23162,23164],{"id":23163},"_3-只看-heap-dump不看时间趋势","3. 只看 Heap Dump，不看时间趋势",[18,23166,23167],{},"单份 Dump 能说明“当时谁占内存”，但不一定能证明谁在持续增长。需要结合多个时间点、GC 后基线和业务流量。",[473,23169,23171],{"id":23170},"_4-把容器内存全部分给-xmx","4. 把容器内存全部分给 Xmx",[18,23173,23174],{},"JVM 还需要 Metaspace、直接内存、线程栈和原生结构，容器必须保留余量。",[473,23176,23178],{"id":23177},"_5-在线高峰期随意执行重型诊断","5. 在线高峰期随意执行重型诊断",[18,23180,23181],{},"Heap Dump、对象直方图等操作可能产生停顿和磁盘压力，应评估实例状态、可用副本和磁盘空间。",[14,23183,23185],{"id":23184},"十六排查检查清单","十六、排查检查清单",[159,23187,23188,23191,23194,23197,23200,23203,23206,23209,23212,23215,23218,23221],{},[162,23189,23190],{},"OOM 的完整错误信息是什么；",[162,23192,23193],{},"是 JVM 抛出的 OOM，还是容器 OOMKilled；",[162,23195,23196],{},"故障发生在哪个内存区域；",[162,23198,23199],{},"是否保存了 Heap Dump、GC 日志和监控；",[162,23201,23202],{},"关键 GC 周期后老年代能否回落；G1 下重点观察并发标记及后续一系列 Mixed GC；",[162,23204,23205],{},"最大对象及其 Retained Size 是多少；",[162,23207,23208],{},"对象通过什么引用链连接到 GC Roots；",[162,23210,23211],{},"类数量、ClassLoader、直接内存或线程数是否持续增长；",[162,23213,23214],{},"最近是否有发布、配置、流量或批量任务变化；",[162,23216,23217],{},"问题属于泄漏、容量不足还是异常大对象；",[162,23219,23220],{},"修复后是否在真实负载和长时间运行下验证；",[162,23222,23223],{},"是否补齐自动留证、监控告警和容量保护。",[14,23225,5803],{"id":5803},[159,23227,23228,23235,23242,23249],{},[162,23229,23230],{},[119,23231,23234],{"href":23232,"rel":23233},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F21\u002Ftroubleshoot\u002Ftroubleshooting-memory-leaks.html",[2080],"Oracle Java Troubleshooting Guide：Troubleshoot Memory Leaks",[162,23236,23237],{},[119,23238,23241],{"href":23239,"rel":23240},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F21\u002Fdocs\u002Fspecs\u002Fman\u002Fjcmd.html",[2080],"Oracle Java Tools Reference：jcmd",[162,23243,23244],{},[119,23245,23248],{"href":23246,"rel":23247},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F21\u002Fvm\u002Fnative-memory-tracking.html",[2080],"Oracle Java Troubleshooting Guide：Native Memory Tracking",[162,23250,23251],{},[119,23252,23255],{"href":23253,"rel":23254},"https:\u002F\u002Feclipse.dev\u002Fmat\u002F",[2080],"Eclipse Memory Analyzer",[2111,23257,23258],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":150,"searchDepth":251,"depth":251,"links":23260},[23261,23262,23263,23264,23265,23266,23272,23276,23281,23282,23283,23284,23285,23286,23287,23292,23293,23298,23305,23306],{"id":16,"depth":251,"text":16},{"id":115,"depth":251,"text":115},{"id":132,"depth":251,"text":132},{"id":21970,"depth":251,"text":21971},{"id":22079,"depth":251,"text":22080},{"id":22109,"depth":251,"text":22110,"children":23267},[23268,23269,23270,23271],{"id":22113,"depth":260,"text":22114},{"id":22146,"depth":260,"text":22147},{"id":22174,"depth":260,"text":22175},{"id":22202,"depth":260,"text":22203},{"id":22339,"depth":251,"text":22340,"children":23273},[23274,23275],{"id":22346,"depth":260,"text":22347},{"id":22360,"depth":260,"text":22361},{"id":22387,"depth":251,"text":22388,"children":23277},[23278,23279,23280],{"id":22400,"depth":260,"text":22401},{"id":22566,"depth":260,"text":22567},{"id":22610,"depth":260,"text":22611},{"id":22640,"depth":251,"text":22641},{"id":22667,"depth":251,"text":22668},{"id":22742,"depth":251,"text":22743},{"id":22800,"depth":251,"text":22801},{"id":22873,"depth":251,"text":22874},{"id":22900,"depth":251,"text":22901},{"id":22969,"depth":251,"text":22970,"children":23288},[23289,23290,23291],{"id":22973,"depth":260,"text":22973},{"id":22993,"depth":260,"text":22993},{"id":23013,"depth":260,"text":23013},{"id":23036,"depth":251,"text":23037},{"id":23069,"depth":251,"text":23070,"children":23294},[23295,23296,23297],{"id":23073,"depth":260,"text":23074},{"id":23091,"depth":260,"text":23091},{"id":23125,"depth":260,"text":23125},{"id":23145,"depth":251,"text":23146,"children":23299},[23300,23301,23302,23303,23304],{"id":23149,"depth":260,"text":23150},{"id":23156,"depth":260,"text":23157},{"id":23163,"depth":260,"text":23164},{"id":23170,"depth":260,"text":23171},{"id":23177,"depth":260,"text":23178},{"id":23184,"depth":251,"text":23185},{"id":5803,"depth":251,"text":5803},"wiki:java:java-oom-troubleshooting","从线上止损、现场保留、OOM 类型识别、Heap Dump 与 GC 日志分析，到堆、元空间、直接内存、线程和容器内存定位，系统讲解 Java OOM 的处理方法。",{},81,"\u002Fjava\u002Fjava-oom-troubleshooting",{"title":21906,"description":23308},"java\u002Fjava-oom-troubleshooting","2026-07-28","y7MSNq1nIooPfRKJm5-oCcbDEXb3CosxwJ-RFf7BvaI",{"id":23317,"title":23318,"body":23319,"commentId":25247,"description":25248,"difficulty":3667,"draft":2148,"extension":2149,"meta":25249,"navigation":756,"order":25250,"path":25251,"section":2153,"seo":25252,"stem":25253,"updated":25254,"__hash__":25255},"java\u002Fjava\u002Fjava-high-cpu-troubleshooting.md","如果一个系统 CPU 飙高，应该如何排查处理？",{"type":7,"value":23320,"toc":25198},[23321,23324,23326,23329,23347,23350,23353,23355,23362,23364,23373,23375,23379,23385,23388,23391,23404,23407,23449,23461,23465,23468,23479,23482,23517,23520,23524,23527,23542,23545,23565,23569,23573,23576,23616,23619,23672,23675,23753,23756,23760,23763,23780,23787,23791,23794,23852,23858,23862,23866,23868,23890,23892,23920,23930,23937,23953,23956,23962,23966,23969,23998,24001,24048,24051,24057,24062,24078,24082,24085,24095,24106,24110,24114,24117,24128,24130,24174,24177,24181,24184,24216,24219,24223,24226,24232,24235,24239,24242,24259,24262,24266,24269,24283,24286,24290,24293,24310,24313,24317,24324,24327,24359,24362,24385,24399,24403,24406,24409,24445,24448,24462,24465,24469,24472,24489,24495,24498,24501,24514,24517,24520,24553,24559,24562,24579,24582,24586,24590,24593,24610,24613,24656,24662,24694,24697,24703,24707,24710,24717,24721,24724,24738,24744,24747,24802,24805,24809,24812,24829,24832,24835,24858,24861,24871,24877,24881,24984,24988,24991,25017,25020,25024,25027,25056,25071,25075,25079,25082,25086,25089,25093,25101,25105,25108,25112,25115,25119,25122,25128,25130,25195],[10,23322,23318],{"id":23323},"如果一个系统-cpu-飙高应该如何排查处理",[14,23325,16],{"id":16},[18,23327,23328],{},"系统 CPU 飙高时，我会先确认影响范围并止损，通过摘除异常实例、限流、降级或临时扩容保护系统，同时保存故障时间、流量、发布变更、CPU、负载、GC、线程数和容器限流等现场数据。CPU 高不一定来自当前 Java 进程，因此要先从机器、容器和进程三个层级确认消耗主体；如果机器 CPU 高而 JVM CPU 不高，应继续检查同机其他进程、内核态、I\u002FO wait 和 steal time。",[18,23330,23331,23332,6265,23335,23338,23339,23342,23343,23346],{},"确认是 Java 进程后，我会定位高 CPU 线程。可以通过 ",[22,23333,23334],{},"top -H -p \u003Cpid>",[22,23336,23337],{},"ps -Lp"," 找到线程 ID，将十进制 TID 转成十六进制，再与 ",[22,23340,23341],{},"jcmd \u003Cpid> Thread.print -l"," 中的 ",[22,23344,23345],{},"nid"," 对应。线程栈要间隔数秒连续采集多次，只有持续出现在相同执行位置的热点线程，才更可能是死循环、频繁重试、正则回溯、序列化、加解密或其他热点代码。",[18,23348,23349],{},"如果没有稳定的单个热点线程，或者问题持续时间很短，我会使用 JFR 或 async-profiler 做一段时间的采样，通过 Hot Methods、Call Tree 或 CPU 火焰图确认 CPU 时间消耗在哪里。同时结合 GC 日志和分配分析判断是否是对象分配过快导致 GC 消耗 CPU；结合线程数、上下文切换和锁事件判断是否是线程膨胀、CAS 自旋或锁竞争；还要检查 JIT 编译线程、原生库和内核调用。",[18,23351,23352],{},"修复时要针对根因处理，而不是只增加 CPU：死循环和无上限重试要修正退出条件并增加退避；高分配要减少临时对象和批次大小；热点计算要优化算法或缓存结果；线程膨胀要设置线程池和队列上限；流量超出容量则需要限流、扩容和容量规划。最后使用接近真实的流量验证 CPU、吞吐、延迟、GC 和错误率是否恢复，并补齐分层 CPU、线程、GC、容器限流和热点方法监控。",[18,23354,103],{},[105,23356,23357],{},[18,23358,23359],{},[110,23360,23361],{},"CPU 飙高的排查主线是先止损，再确认是机器、容器还是 JVM 消耗 CPU，然后把高 CPU 线程关联到 Java 栈或火焰图，最后结合 GC、线程和流量定位根因。",[14,23363,115],{"id":115},[18,23365,23366],{},[119,23367,23370],{"href":23368,"target":122,"rel":23369},"\u002Fimages\u002Fwiki\u002Fjava\u002Fjava-high-cpu-troubleshooting.svg",[124,125],[127,23371],{"src":23368,"alt":23372},"Java 系统 CPU 飙高的止损、分层定位、线程关联、根因分析和修复验证流程",[14,23374,132],{"id":132},[473,23376,23378],{"id":23377},"先理解容器-cpu-限流","先理解容器 CPU 限流",[18,23380,23381,23382,33],{},"在容器环境中，CPU 使用量会受到 cgroup 配额约束。容器在一个统计周期内用完可用 CPU 时间后，内核会暂时停止调度其中的线程，等下一个周期再恢复运行，这就是",[110,23383,23384],{},"容器 CPU 限流（CPU throttling）",[18,23386,23387],{},"它不代表应用主动休眠，也不一定表现为整机 CPU 100%。即使宿主机仍有空闲 CPU，只要容器自己的配额已经耗尽，应用也会被限制运行，接口延迟和任务处理时间可能因此升高。",[18,23389,23390],{},"在 cgroup v2 中可以查看：",[144,23392,23394],{"className":7980,"code":23393,"language":7982,"meta":150,"style":150},"cat \u002Fsys\u002Ffs\u002Fcgroup\u002Fcpu.stat\n",[22,23395,23396],{"__ignoreMap":150},[203,23397,23398,23401],{"class":205,"line":206},[203,23399,23400],{"class":215},"cat",[203,23402,23403],{"class":223}," \u002Fsys\u002Ffs\u002Fcgroup\u002Fcpu.stat\n",[18,23405,23406],{},"常见字段包括：",[299,23408,23409,23417],{},[302,23410,23411],{},[305,23412,23413,23415],{},[308,23414,2550],{},[308,23416,2553],{},[315,23418,23419,23429,23439],{},[305,23420,23421,23426],{},[320,23422,23423],{},[22,23424,23425],{},"nr_periods",[320,23427,23428],{},"已经经过的 CPU 配额统计周期数",[305,23430,23431,23436],{},[320,23432,23433],{},[22,23434,23435],{},"nr_throttled",[320,23437,23438],{},"容器在多少个周期内触发过 CPU 限流",[305,23440,23441,23446],{},[320,23442,23443],{},[22,23444,23445],{},"throttled_usec",[320,23447,23448],{},"容器累计被限制运行的时间，单位为微秒",[18,23450,23451,23452,23454,23455,23457,23458,23460],{},"排查时应观察这些计数在一段时间内的增长速度，而不是只看某个累计值。",[22,23453,23435],{}," 占 ",[22,23456,23425],{}," 的比例持续较高，或 ",[22,23459,23445],{}," 快速增长，说明容器经常受 CPU 配额限制；还要继续判断是 CPU limit 配置过低，还是应用存在异常计算。",[14,23462,23464],{"id":23463},"一cpu-高并不等于业务代码一定有死循环","一、CPU 高并不等于业务代码一定有死循环",[18,23466,23467],{},"看到 CPU 告警后，首先要明确三个问题：",[2695,23469,23470,23473,23476],{},[162,23471,23472],{},"是整台机器 CPU 高，还是某个容器 CPU 高？",[162,23474,23475],{},"是 Java 进程消耗 CPU，还是同机其他进程或内核消耗 CPU？",[162,23477,23478],{},"是真实计算压力增加，还是容器 CPU 配额不足、线程调度或 GC 等问题？",[18,23480,23481],{},"常见根因包括：",[159,23483,23484,23487,23490,23493,23496,23499,23502,23505,23508,23511,23514],{},[162,23485,23486],{},"流量或任务量突然上升；",[162,23488,23489],{},"死循环、无上限重试或消息空转；",[162,23491,23492],{},"正则表达式灾难性回溯；",[162,23494,23495],{},"JSON 序列化、压缩、加解密等计算热点；",[162,23497,23498],{},"对象分配速度过快，引发频繁 GC；",[162,23500,23501],{},"线程数量膨胀和上下文切换；",[162,23503,23504],{},"CAS 竞争、自旋等待或热点锁；",[162,23506,23507],{},"异常大量抛出、堆栈生成和同步日志输出；",[162,23509,23510],{},"JIT 编译、GC 线程或第三方原生库消耗 CPU；",[162,23512,23513],{},"其他进程、内核中断或虚拟机宿主机争抢 CPU；",[162,23515,23516],{},"容器 CPU limit 太低或持续发生 CPU 限流。",[18,23518,23519],{},"因此不能一看到 CPU 高，就直接重启、扩容或者抓一份线程栈后认定某段代码有问题。",[14,23521,23523],{"id":23522},"二第一阶段先控制故障影响","二、第一阶段：先控制故障影响",[18,23525,23526],{},"线上处理应先恢复系统可用性：",[2695,23528,23529,23532,23534,23537,23539],{},[162,23530,23531],{},"从负载均衡中摘除最异常的实例；",[162,23533,22094],{},[162,23535,23536],{},"暂停可能造成空转、重试风暴或计算放大的任务；",[162,23538,22091],{},[162,23540,23541],{},"如果线程已经长期占满 CPU 且服务不可用，保留必要现场后重启实例。",[18,23543,23544],{},"重启能够暂时清除异常状态，但也会丢失最有价值的线程和性能现场。条件允许时，至少先保存：",[159,23546,23547,23550,23553,23556,23559,23562],{},[162,23548,23549],{},"告警开始和结束时间；",[162,23551,23552],{},"进程 CPU、机器 CPU、负载和容器 CPU；",[162,23554,23555],{},"请求量、任务量、消息消费速率和错误率；",[162,23557,23558],{},"线程数、GC 次数和 GC 线程消耗的 CPU；",[162,23560,23561],{},"最近的发布、配置和数据变更；",[162,23563,23564],{},"多份线程栈或一段 JFR、async-profiler 记录。",[14,23566,23568],{"id":23567},"三第二阶段确认-cpu-到底消耗在哪里","三、第二阶段：确认 CPU 到底消耗在哪里",[473,23570,23572],{"id":23571},"_1-机器层面","1. 机器层面",[18,23574,23575],{},"先查看整机情况：",[144,23577,23579],{"className":7980,"code":23578,"language":7982,"meta":150,"style":150},"top\nmpstat -P ALL 1\npidstat -u 1\nvmstat 1\n",[22,23580,23581,23586,23599,23609],{"__ignoreMap":150},[203,23582,23583],{"class":205,"line":206},[203,23584,23585],{"class":215},"top\n",[203,23587,23588,23591,23594,23597],{"class":205,"line":251},[203,23589,23590],{"class":215},"mpstat",[203,23592,23593],{"class":718}," -P",[203,23595,23596],{"class":223}," ALL",[203,23598,11460],{"class":718},[203,23600,23601,23604,23607],{"class":205,"line":260},[203,23602,23603],{"class":215},"pidstat",[203,23605,23606],{"class":718}," -u",[203,23608,11460],{"class":718},[203,23610,23611,23614],{"class":205,"line":278},[203,23612,23613],{"class":215},"vmstat",[203,23615,11460],{"class":718},[18,23617,23618],{},"这些命令观察的层次不同：",[299,23620,23621,23630],{},[302,23622,23623],{},[305,23624,23625,23628],{},[308,23626,23627],{},"命令",[308,23629,15568],{},[315,23631,23632,23642,23652,23662],{},[305,23633,23634,23639],{},[320,23635,23636],{},[22,23637,23638],{},"top",[320,23640,23641],{},"查看整机负载、CPU 和内存概况，并找出资源占用较高的进程",[305,23643,23644,23649],{},[320,23645,23646],{},[22,23647,23648],{},"mpstat -P ALL 1",[320,23650,23651],{},"每秒输出一次每个 CPU 核的使用情况，用于判断负载是否集中在少数核心，并区分用户态、内核态、I\u002FO wait 和 steal time",[305,23653,23654,23659],{},[320,23655,23656],{},[22,23657,23658],{},"pidstat -u 1",[320,23660,23661],{},"每秒按进程输出一次 CPU 使用情况，用于确认哪个进程正在消耗 CPU",[305,23663,23664,23669],{},[320,23665,23666],{},[22,23667,23668],{},"vmstat 1",[320,23670,23671],{},"每秒输出一次系统运行队列、内存、交换、I\u002FO、上下文切换和 CPU 概况，用于判断 CPU 高是否伴随排队、换页或 I\u002FO 压力",[18,23673,23674],{},"重点区分：",[299,23676,23677,23688],{},[302,23678,23679],{},[305,23680,23681,23683,23685],{},[308,23682,7029],{},[308,23684,2553],{},[308,23686,23687],{},"常见方向",[315,23689,23690,23703,23716,23729,23742],{},[305,23691,23692,23697,23700],{},[320,23693,23694],{},[22,23695,23696],{},"us",[320,23698,23699],{},"用户态 CPU",[320,23701,23702],{},"Java 业务代码、GC、JIT 或其他用户进程",[305,23704,23705,23710,23713],{},[320,23706,23707],{},[22,23708,23709],{},"sy",[320,23711,23712],{},"内核态 CPU",[320,23714,23715],{},"系统调用、网络、磁盘、驱动或内核开销",[305,23717,23718,23723,23726],{},[320,23719,23720],{},[22,23721,23722],{},"wa",[320,23724,23725],{},"CPU 等待 I\u002FO 的时间占比",[320,23727,23728],{},"磁盘或存储瓶颈，不等同于 CPU 计算饱和",[305,23730,23731,23736,23739],{},[320,23732,23733],{},[22,23734,23735],{},"st",[320,23737,23738],{},"虚拟机被宿主机拿走的 CPU 时间",[320,23740,23741],{},"宿主机资源争抢",[305,23743,23744,23747,23750],{},[320,23745,23746],{},"Load Average",[320,23748,23749],{},"正在运行或不可中断等待的任务压力",[320,23751,23752],{},"不是 CPU 使用率，需结合 CPU 核数和任务状态判断",[18,23754,23755],{},"如果机器 CPU 很高，但目标 JVM CPU 不高，应先找出真正的高 CPU 进程，而不是继续分析 Java 线程。",[473,23757,23759],{"id":23758},"_2-容器层面","2. 容器层面",[18,23761,23762],{},"在 Kubernetes 或其他容器环境中，还要关注：",[159,23764,23765,23768,23771,23774,23777],{},[162,23766,23767],{},"Pod 实际 CPU 使用量；",[162,23769,23770],{},"CPU request 和 limit；",[162,23772,23773],{},"容器是否发生 CPU 限流；",[162,23775,23776],{},"Node 是否整体繁忙；",[162,23778,23779],{},"Pod 是否频繁扩缩容、重启或迁移。",[18,23781,23782,23783,23786],{},"前文已经介绍了 CPU 限流的含义和 ",[22,23784,23785],{},"cpu.stat"," 指标。这里还应将 Pod 的 CPU 使用量、limit、限流时间与业务延迟放到同一时间线上，避免只根据某一时刻的 CPU 百分比判断。",[473,23788,23790],{"id":23789},"_3-进程层面","3. 进程层面",[18,23792,23793],{},"确认 Java 进程：",[144,23795,23797],{"className":7980,"code":23796,"language":7982,"meta":150,"style":150},"# 每秒查看一次指定进程的用户态、内核态及总 CPU 使用率\npidstat -u -p \u003Cpid> 1\n\n# 查看进程 ID、父进程 ID、CPU、内存、线程数和启动命令\nps -p \u003Cpid> -o pid,ppid,%cpu,%mem,nlwp,cmd\n",[22,23798,23799,23804,23823,23827,23832],{"__ignoreMap":150},[203,23800,23801],{"class":205,"line":206},[203,23802,23803],{"class":281},"# 每秒查看一次指定进程的用户态、内核态及总 CPU 使用率\n",[203,23805,23806,23808,23810,23813,23815,23817,23819,23821],{"class":205,"line":251},[203,23807,23603],{"class":215},[203,23809,23606],{"class":718},[203,23811,23812],{"class":718}," -p",[203,23814,22216],{"class":209},[203,23816,22219],{"class":223},[203,23818,22222],{"class":219},[203,23820,9371],{"class":209},[203,23822,11460],{"class":718},[203,23824,23825],{"class":205,"line":260},[203,23826,757],{"emptyLinePlaceholder":756},[203,23828,23829],{"class":205,"line":278},[203,23830,23831],{"class":281},"# 查看进程 ID、父进程 ID、CPU、内存、线程数和启动命令\n",[203,23833,23834,23836,23838,23840,23842,23844,23846,23849],{"class":205,"line":285},[203,23835,22864],{"class":215},[203,23837,23812],{"class":718},[203,23839,22216],{"class":209},[203,23841,22219],{"class":223},[203,23843,22222],{"class":219},[203,23845,9371],{"class":209},[203,23847,23848],{"class":718}," -o",[203,23850,23851],{"class":223}," pid,ppid,%cpu,%mem,nlwp,cmd\n",[18,23853,23854,23857],{},[22,23855,23856],{},"%CPU"," 在多核系统中可能超过 100%。例如一个进程稳定使用两个 CPU 核，在某些工具中可能显示约 200%，不能机械地把超过 100% 当成监控错误。",[14,23859,23861],{"id":23860},"四第三阶段定位高-cpu-线程","四、第三阶段：定位高 CPU 线程",[473,23863,23865],{"id":23864},"_1-找到进程内的热点线程","1. 找到进程内的热点线程",[18,23867,21108],{},[144,23869,23871],{"className":7980,"code":23870,"language":7982,"meta":150,"style":150},"top -H -p \u003Cpid>\n",[22,23872,23873],{"__ignoreMap":150},[203,23874,23875,23877,23880,23882,23884,23886,23888],{"class":205,"line":206},[203,23876,23638],{"class":215},[203,23878,23879],{"class":718}," -H",[203,23881,23812],{"class":718},[203,23883,22216],{"class":209},[203,23885,22219],{"class":223},[203,23887,22222],{"class":219},[203,23889,3313],{"class":209},[18,23891,3746],{},[144,23893,23895],{"className":7980,"code":23894,"language":7982,"meta":150,"style":150},"ps -Lp \u003Cpid> -o pid,tid,pcpu,stat,comm --sort=-pcpu\n",[22,23896,23897],{"__ignoreMap":150},[203,23898,23899,23901,23904,23906,23908,23910,23912,23914,23917],{"class":205,"line":206},[203,23900,22864],{"class":215},[203,23902,23903],{"class":718}," -Lp",[203,23905,22216],{"class":209},[203,23907,22219],{"class":223},[203,23909,22222],{"class":219},[203,23911,9371],{"class":209},[203,23913,23848],{"class":718},[203,23915,23916],{"class":223}," pid,tid,pcpu,stat,comm",[203,23918,23919],{"class":718}," --sort=-pcpu\n",[18,23921,23922,23923,23926,23927,23929],{},"记录 CPU 最高的 TID。",[110,23924,23925],{},"TID（Thread ID）是操作系统为线程分配的标识","；在 Linux 中，Java 线程会对应一个可调度的本地线程，因此可以先按 TID 找到消耗 CPU 的操作系统线程，再通过 JVM 线程栈中的 ",[22,23928,23345],{}," 定位对应的 Java 调用栈。",[18,23931,23932,23933,23936],{},"假设十进制线程 ID 为 ",[22,23934,23935],{},"12345","，转换成十六进制：",[144,23938,23940],{"className":7980,"code":23939,"language":7982,"meta":150,"style":150},"printf '%x\\n' 12345\n",[22,23941,23942],{"__ignoreMap":150},[203,23943,23944,23947,23950],{"class":205,"line":206},[203,23945,23946],{"class":718},"printf",[203,23948,23949],{"class":223}," '%x\\n'",[203,23951,23952],{"class":718}," 12345\n",[18,23954,23955],{},"输出：",[144,23957,23960],{"className":23958,"code":23959,"language":149,"meta":150},[147],"3039\n",[22,23961,23959],{"__ignoreMap":150},[473,23963,23965],{"id":23964},"_2-获取-java-线程栈","2. 获取 Java 线程栈",[18,23967,23968],{},"推荐使用：",[144,23970,23972],{"className":7980,"code":23971,"language":7982,"meta":150,"style":150},"jcmd \u003Cpid> Thread.print -l > thread-1.txt\n",[22,23973,23974],{"__ignoreMap":150},[203,23975,23976,23978,23980,23982,23984,23986,23989,23992,23995],{"class":205,"line":206},[203,23977,22213],{"class":215},[203,23979,22216],{"class":209},[203,23981,22219],{"class":223},[203,23983,22222],{"class":219},[203,23985,9371],{"class":209},[203,23987,23988],{"class":223}," Thread.print",[203,23990,23991],{"class":718}," -l",[203,23993,23994],{"class":209}," >",[203,23996,23997],{"class":223}," thread-1.txt\n",[18,23999,24000],{},"间隔数秒再采集：",[144,24002,24004],{"className":7980,"code":24003,"language":7982,"meta":150,"style":150},"jcmd \u003Cpid> Thread.print -l > thread-2.txt\njcmd \u003Cpid> Thread.print -l > thread-3.txt\n",[22,24005,24006,24027],{"__ignoreMap":150},[203,24007,24008,24010,24012,24014,24016,24018,24020,24022,24024],{"class":205,"line":206},[203,24009,22213],{"class":215},[203,24011,22216],{"class":209},[203,24013,22219],{"class":223},[203,24015,22222],{"class":219},[203,24017,9371],{"class":209},[203,24019,23988],{"class":223},[203,24021,23991],{"class":718},[203,24023,23994],{"class":209},[203,24025,24026],{"class":223}," thread-2.txt\n",[203,24028,24029,24031,24033,24035,24037,24039,24041,24043,24045],{"class":205,"line":251},[203,24030,22213],{"class":215},[203,24032,22216],{"class":209},[203,24034,22219],{"class":223},[203,24036,22222],{"class":219},[203,24038,9371],{"class":209},[203,24040,23988],{"class":223},[203,24042,23991],{"class":718},[203,24044,23994],{"class":209},[203,24046,24047],{"class":223}," thread-3.txt\n",[18,24049,24050],{},"然后在文件中查找：",[144,24052,24055],{"className":24053,"code":24054,"language":149,"meta":150},[147],"nid=0x3039\n",[22,24056,24054],{"__ignoreMap":150},[18,24058,24059,24061],{},[22,24060,23345],{}," 是 JVM 线程对应的本地线程 ID，通常以十六进制展示。找到线程后，要同时观察：",[159,24063,24064,24066,24069,24072,24075],{},[162,24065,17597],{},[162,24067,24068],{},"线程状态；",[162,24070,24071],{},"当前方法和完整调用链；",[162,24073,24074],{},"是否连续多次停留在相同或相邻代码位置；",[162,24076,24077],{},"是否属于业务线程、GC 线程、JIT 编译线程或第三方原生线程。",[473,24079,24081],{"id":24080},"_3-为什么要连续采集多份线程栈","3. 为什么要连续采集多份线程栈",[18,24083,24084],{},"单份线程栈只是一瞬间的快照。一个正常执行请求的线程也可能恰好位于某个计算方法中，不能仅凭一次采样就认定它是根因。",[18,24086,24087,24088,24091,24092,24094],{},"如果同一线程在多份栈中持续处于 ",[22,24089,24090],{},"RUNNABLE","，并反复出现在相同调用路径，同时该线程的 TID CPU 持续较高，才更能说明它正在消耗 CPU。Oracle 的故障排查文档也建议对疑似循环线程采集一系列线程转储，重点观察持续处于 ",[22,24093,24090],{}," 状态的线程。",[18,24096,24097,24099,24100,24102,24103,24105],{},[22,24098,24090],{}," 线程数是线程转储或监控中处于 Java ",[22,24101,24090],{}," 状态的线程数量。它可以辅助发现大量活跃任务、忙循环或线程池膨胀，但不能单独等同于“正在占用 CPU 的线程数”：Java 的 ",[22,24104,24090],{}," 同时覆盖正在执行 Java 代码以及部分处于原生调用中的线程，最终仍要结合线程级 CPU 和连续栈判断。",[14,24107,24109],{"id":24108},"五线程栈中重点看什么","五、线程栈中重点看什么",[473,24111,24113],{"id":24112},"_1-死循环或条件无法退出","1. 死循环或条件无法退出",[18,24115,24116],{},"典型表现：",[159,24118,24119,24122,24125],{},[162,24120,24121],{},"某个线程长期接近占满一个 CPU 核；",[162,24123,24124],{},"多次线程栈都停在同一段业务代码；",[162,24126,24127],{},"请求量下降后 CPU 仍不回落。",[18,24129,686],{},[144,24131,24133],{"className":197,"code":24132,"language":199,"meta":150,"style":150},"while (running) {\n    if (queue.poll() == null) {\n        continue;\n    }\n}\n",[22,24134,24135,24142,24159,24166,24170],{"__ignoreMap":150},[203,24136,24137,24139],{"class":205,"line":206},[203,24138,4479],{"class":209},[203,24140,24141],{"class":219}," (running) {\n",[203,24143,24144,24146,24149,24151,24153,24155,24157],{"class":205,"line":251},[203,24145,709],{"class":209},[203,24147,24148],{"class":219}," (queue.",[203,24150,5192],{"class":215},[203,24152,16393],{"class":219},[203,24154,715],{"class":209},[203,24156,719],{"class":718},[203,24158,275],{"class":219},[203,24160,24161,24164],{"class":205,"line":260},[203,24162,24163],{"class":209},"        continue",[203,24165,1297],{"class":219},[203,24167,24168],{"class":205,"line":278},[203,24169,751],{"class":219},[203,24171,24172],{"class":205,"line":285},[203,24173,288],{"class":219},[18,24175,24176],{},"队列为空时仍然持续空转。可以根据场景改为阻塞队列、条件等待或带退避的重试。",[473,24178,24180],{"id":24179},"_2-无上限重试","2. 无上限重试",[18,24182,24183],{},"例如远程调用或 CAS 更新失败后立即重试：",[144,24185,24187],{"className":197,"code":24186,"language":199,"meta":150,"style":150},"while (!success) {\n    success = callRemoteService();\n}\n",[22,24188,24189,24200,24212],{"__ignoreMap":150},[203,24190,24191,24193,24195,24197],{"class":205,"line":206},[203,24192,4479],{"class":209},[203,24194,5163],{"class":219},[203,24196,18379],{"class":209},[203,24198,24199],{"class":219},"success) {\n",[203,24201,24202,24205,24207,24210],{"class":205,"line":251},[203,24203,24204],{"class":219},"    success ",[203,24206,495],{"class":209},[203,24208,24209],{"class":215}," callRemoteService",[203,24211,257],{"class":219},[203,24213,24214],{"class":205,"line":260},[203,24215,288],{"class":219},[18,24217,24218],{},"当下游持续失败时，它会形成重试风暴。应设置最大次数、指数退避、随机抖动、熔断和失败后的补偿路径。",[473,24220,24222],{"id":24221},"_3-正则表达式回溯","3. 正则表达式回溯",[18,24224,24225],{},"复杂正则在特定输入下可能产生大量回溯，线程栈常出现：",[144,24227,24230],{"className":24228,"code":24229,"language":149,"meta":150},[147],"java.util.regex.Pattern\njava.util.regex.Matcher\n",[22,24231,24229],{"__ignoreMap":150},[18,24233,24234],{},"需要结合具体表达式、输入数据和耗时复现，不能简单认为所有正则调用都有问题。",[473,24236,24238],{"id":24237},"_4-序列化压缩加解密和大集合计算","4. 序列化、压缩、加解密和大集合计算",[18,24240,24241],{},"常见热点包括：",[159,24243,24244,24247,24250,24253,24256],{},[162,24245,24246],{},"大 JSON 的序列化和反序列化；",[162,24248,24249],{},"GZIP 压缩与解压；",[162,24251,24252],{},"哈希、签名和加解密；",[162,24254,24255],{},"大集合排序、去重和嵌套遍历；",[162,24257,24258],{},"图片、音视频或文档转换。",[18,24260,24261],{},"这类问题通常不是线程异常，而是单位请求计算量过大或流量超过容量。需要从算法、数据规模、缓存、批次和并行度处理。",[473,24263,24265],{"id":24264},"_5-异常和日志风暴","5. 异常和日志风暴",[18,24267,24268],{},"频繁创建异常会生成堆栈信息；大量同步日志还会增加格式化、锁和 I\u002FO 开销。监控中常伴随：",[159,24270,24271,24274,24277,24280],{},[162,24272,24273],{},"异常计数骤增；",[162,24275,24276],{},"日志量骤增；",[162,24278,24279],{},"相同错误重复打印；",[162,24281,24282],{},"CPU 与错误率同步上升。",[18,24284,24285],{},"应修复异常根因，同时对可预期失败避免无意义的完整堆栈，并对重复日志做采样或限速。",[14,24287,24289],{"id":24288},"六没有明显热点线程时怎么办","六、没有明显热点线程时怎么办",[18,24291,24292],{},"线程 ID 与线程栈关联适合定位持续占用 CPU 的线程，但以下场景可能看不到稳定热点：",[159,24294,24295,24298,24301,24304,24307],{},[162,24296,24297],{},"大量短任务分散到多个线程；",[162,24299,24300],{},"热点线程变化很快；",[162,24302,24303],{},"问题只持续几十秒；",[162,24305,24306],{},"CPU 消耗发生在 GC、JIT、原生库或内核中；",[162,24308,24309],{},"CPU 时间分散在多个方法上，没有单一调用栈长期占用 CPU。",[18,24311,24312],{},"这时更适合使用采样分析。",[473,24314,24316],{"id":24315},"_1-使用-jfr","1. 使用 JFR",[18,24318,24319,24320,24323],{},"JFR（Java Flight Recorder）是 JDK 内置的低开销事件记录器。它会在一段时间内记录 CPU 采样、线程、GC、对象分配、同步和 I\u002FO 等事件，生成 ",[22,24321,24322],{},".jfr"," 文件，再通过 JDK Mission Control 分析时间趋势、热点方法和调用关系。这里所说的“一段 JFR 记录”，就是限定时间采集到的一份 JVM 运行现场。",[18,24325,24326],{},"可以对运行中的 JVM 启动一段性能记录：",[144,24328,24330],{"className":7980,"code":24329,"language":7982,"meta":150,"style":150},"jcmd \u003Cpid> JFR.start name=cpu-check settings=profile duration=60s filename=\u002Ftmp\u002Fcpu-check.jfr\n",[22,24331,24332],{"__ignoreMap":150},[203,24333,24334,24336,24338,24340,24342,24344,24347,24350,24353,24356],{"class":205,"line":206},[203,24335,22213],{"class":215},[203,24337,22216],{"class":209},[203,24339,22219],{"class":223},[203,24341,22222],{"class":219},[203,24343,9371],{"class":209},[203,24345,24346],{"class":223}," JFR.start",[203,24348,24349],{"class":223}," name=cpu-check",[203,24351,24352],{"class":223}," settings=profile",[203,24354,24355],{"class":223}," duration=60s",[203,24357,24358],{"class":223}," filename=\u002Ftmp\u002Fcpu-check.jfr\n",[18,24360,24361],{},"在 JDK Mission Control 中重点查看：",[159,24363,24364,24370,24376,24379,24382],{},[162,24365,24366,24369],{},[22,24367,24368],{},"jdk.CPULoad","：JVM 和机器 CPU 趋势；",[162,24371,24372,24375],{},[22,24373,24374],{},"jdk.ThreadCPULoad","：线程 CPU 使用；",[162,24377,24378],{},"Hot Methods：热点方法；",[162,24380,24381],{},"Call Tree：热点方法的调用来源；",[162,24383,24384],{},"GC、分配、锁竞争和线程事件。",[18,24386,24387,24388,24391,24392,24395,24396,24398],{},"JFR 是采样和事件记录工具，不保证每次短暂热点都能被完整捕获。",[22,24389,24390],{},"default"," 配置偏向持续、低开销记录，",[22,24393,24394],{},"profile"," 配置会采集更多性能信息，开销也相对更高。采样数量过少时，应延长记录时间或在可控范围内使用 ",[22,24397,24394],{}," 配置。",[473,24400,24402],{"id":24401},"_2-使用-async-profiler","2. 使用 async-profiler",[18,24404,24405],{},"async-profiler 是面向 HotSpot JVM 的低开销采样分析器，可以按固定频率采集 CPU、分配、锁等事件，并输出火焰图。这里所说的“一段 async-profiler 记录”，是指在问题发生期间采样数十秒，将大量调用栈汇总为热点分布，而不是只抓取某一个瞬间的线程栈。",[18,24407,24408],{},"CPU 火焰图示例：",[144,24410,24412],{"className":7980,"code":24411,"language":7982,"meta":150,"style":150},"asprof -e cpu -d 30 -f \u002Ftmp\u002Fcpu-flamegraph.html \u003Cpid>\n",[22,24413,24414],{"__ignoreMap":150},[203,24415,24416,24419,24422,24425,24428,24431,24434,24437,24439,24441,24443],{"class":205,"line":206},[203,24417,24418],{"class":215},"asprof",[203,24420,24421],{"class":718}," -e",[203,24423,24424],{"class":223}," cpu",[203,24426,24427],{"class":718}," -d",[203,24429,24430],{"class":718}," 30",[203,24432,24433],{"class":718}," -f",[203,24435,24436],{"class":223}," \u002Ftmp\u002Fcpu-flamegraph.html",[203,24438,22216],{"class":209},[203,24440,22219],{"class":223},[203,24442,22222],{"class":219},[203,24444,3313],{"class":209},[18,24446,24447],{},"火焰图中：",[159,24449,24450,24453,24456,24459],{},[162,24451,24452],{},"横向宽度表示该调用栈被采样到的比例，不表示时间先后；",[162,24454,24455],{},"纵向表示调用栈深度；",[162,24457,24458],{},"越宽的方法越值得优先分析；",[162,24460,24461],{},"应从宽栈向下查看是谁调用了热点方法，而不是只修改最顶部的方法。",[18,24463,24464],{},"async-profiler 还能看到部分原生和内核栈，适合 Java 栈无法解释 CPU 去向的场景。",[14,24466,24468],{"id":24467},"七判断是不是-gc-导致-cpu-高","七、判断是不是 GC 导致 CPU 高",[18,24470,24471],{},"对象创建速度过快时，GC 线程可能消耗大量 CPU。此时通常同时出现：",[159,24473,24474,24477,24480,24483,24486],{},[162,24475,24476],{},"分配速率明显升高；",[162,24478,24479],{},"Young GC 变得频繁；",[162,24481,24482],{},"GC CPU 占比升高；",[162,24484,24485],{},"吞吐下降但堆未必持续增长；",[162,24487,24488],{},"热点线程可能是 GC 工作线程，而不是业务线程。",[18,24490,8521,24491,24494],{},[110,24492,24493],{},"GC CPU"," 是诊断中的简写，不是所有平台都具有完全一致口径的单一 JVM 指标。它通常表示 GC 线程执行并发标记、复制、整理、引用处理等垃圾回收工作所消耗的 CPU 时间或 CPU 占比。",[18,24496,24497],{},"GC CPU 与 GC 停顿不是同一个概念：停顿时间描述应用线程被暂停多久，GC CPU 描述垃圾回收工作消耗了多少计算资源。并发收集器可能在停顿不长时仍消耗较多 CPU，因此要把 GC 日志中的次数和阶段、JFR 的 GC 事件、进程或线程 CPU，以及分配速率结合起来判断。",[18,24499,24500],{},"GC 日志不是业务代码通过日志框架主动打印的，而是 JVM 自身输出的诊断日志，但通常需要在 JVM 启动参数中提前开启。JDK 9 及以上可以使用统一日志参数，例如：",[144,24502,24504],{"className":7980,"code":24503,"language":7982,"meta":150,"style":150},"-Xlog:gc*:file=\u002Fvar\u002Flog\u002Fapp\u002Fgc.log:time,uptime,level,tags:filecount=10,filesize=100M\n",[22,24505,24506],{"__ignoreMap":150},[203,24507,24508,24511],{"class":205,"line":206},[203,24509,24510],{"class":215},"-Xlog:gc*:file",[203,24512,24513],{"class":223},"=\u002Fvar\u002Flog\u002Fapp\u002Fgc.log:time,uptime,level,tags:filecount=10,filesize=100M\n",[18,24515,24516],{},"如果故障前没有开启 GC 日志，就无法事后补回完整的历史 GC 明细，只能结合现有监控、JFR 记录和当前 JVM 状态分析。因此生产环境应提前配置 GC 日志轮转，避免日志无限增长。",[18,24518,24519],{},"需要结合 GC 日志、JFR 和分配采样分析：",[144,24521,24523],{"className":7980,"code":24522,"language":7982,"meta":150,"style":150},"jcmd \u003Cpid> GC.heap_info\njcmd \u003Cpid> GC.class_histogram\n",[22,24524,24525,24539],{"__ignoreMap":150},[203,24526,24527,24529,24531,24533,24535,24537],{"class":205,"line":206},[203,24528,22213],{"class":215},[203,24530,22216],{"class":209},[203,24532,22219],{"class":223},[203,24534,22222],{"class":219},[203,24536,9371],{"class":209},[203,24538,22242],{"class":223},[203,24540,24541,24543,24545,24547,24549,24551],{"class":205,"line":251},[203,24542,22213],{"class":215},[203,24544,22216],{"class":209},[203,24546,22219],{"class":223},[203,24548,22222],{"class":219},[203,24550,9371],{"class":209},[203,24552,22257],{"class":223},[18,24554,24555,24558],{},[22,24556,24557],{},"GC.class_histogram"," 可能带来额外开销，不应在高压环境下无节制频繁执行。更重要的是确认哪些调用路径在高速分配对象，而不是只观察当前对象数量。",[18,24560,24561],{},"常见修复方向：",[159,24563,24564,24567,24570,24573,24576],{},[162,24565,24566],{},"减少循环内临时对象；",[162,24568,24569],{},"避免不必要的装箱、字符串拼接和重复序列化；",[162,24571,24572],{},"限制查询结果、批次和消息大小；",[162,24574,24575],{},"复用代价高但线程安全的对象；",[162,24577,24578],{},"根据真实存活集和分配速率重新评估堆与 GC 参数。",[18,24580,24581],{},"不要在没有证据时首先调整 GC 参数。很多所谓的“GC 问题”，根因其实是业务代码制造对象的速度过快。",[14,24583,24585],{"id":24584},"八线程和锁也可能把-cpu-推高","八、线程和锁也可能把 CPU 推高",[473,24587,24589],{"id":24588},"_1-线程数量过多","1. 线程数量过多",[18,24591,24592],{},"线程越多不代表吞吐越高。大量可运行线程会产生：",[159,24594,24595,24598,24601,24604,24607],{},[162,24596,24597],{},"上下文切换；",[162,24599,24600],{},"调度开销；",[162,24602,24603],{},"CPU 缓存失效；",[162,24605,24606],{},"线程栈和原生内存占用；",[162,24608,24609],{},"请求超时后继续执行，形成任务堆积。",[18,24611,24612],{},"可以观察：",[144,24614,24616],{"className":7980,"code":24615,"language":7982,"meta":150,"style":150},"pidstat -w -p \u003Cpid> 1\nps -p \u003Cpid> -o nlwp\n",[22,24617,24618,24637],{"__ignoreMap":150},[203,24619,24620,24622,24625,24627,24629,24631,24633,24635],{"class":205,"line":206},[203,24621,23603],{"class":215},[203,24623,24624],{"class":718}," -w",[203,24626,23812],{"class":718},[203,24628,22216],{"class":209},[203,24630,22219],{"class":223},[203,24632,22222],{"class":219},[203,24634,9371],{"class":209},[203,24636,11460],{"class":718},[203,24638,24639,24641,24643,24645,24647,24649,24651,24653],{"class":205,"line":251},[203,24640,22864],{"class":215},[203,24642,23812],{"class":718},[203,24644,22216],{"class":209},[203,24646,22219],{"class":223},[203,24648,22222],{"class":219},[203,24650,9371],{"class":209},[203,24652,23848],{"class":718},[203,24654,24655],{"class":223}," nlwp\n",[18,24657,24658,24661],{},[22,24659,24660],{},"pidstat -w"," 用于查看任务切换活动，常见字段包括：",[299,24663,24664,24672],{},[302,24665,24666],{},[305,24667,24668,24670],{},[308,24669,2550],{},[308,24671,2553],{},[315,24673,24674,24684],{},[305,24675,24676,24681],{},[320,24677,24678],{},[22,24679,24680],{},"cswch\u002Fs",[320,24682,24683],{},"每秒自愿上下文切换次数，例如线程因为等待锁、I\u002FO 或条件而主动让出 CPU",[305,24685,24686,24691],{},[320,24687,24688],{},[22,24689,24690],{},"nvcswch\u002Fs",[320,24692,24693],{},"每秒非自愿上下文切换次数，例如线程时间片用完或被更高优先级任务抢占",[18,24695,24696],{},"切换次数升高本身不是根因，但如果它与线程数、CPU 和延迟同步激增，通常说明线程过多、锁竞争频繁或调度压力增大。",[18,24698,24699,24702],{},[22,24700,24701],{},"nlwp"," 表示轻量级进程数量，在 Linux 上可近似理解为该进程的线程数。",[473,24704,24706],{"id":24705},"_2-cas-自旋和热点竞争","2. CAS 自旋和热点竞争",[18,24708,24709],{},"CAS 失败后通常需要重试。多个线程持续竞争同一个变量时，可能产生大量失败和自旋，CPU 很高但有效吞吐不高。",[18,24711,24712,24713,24716],{},"例如高并发统计可以考虑 ",[22,24714,24715],{},"LongAdder"," 分散热点；业务状态更新则要重新评估数据分片、临界区、锁粒度和是否真的需要所有线程竞争同一资源。",[473,24718,24720],{"id":24719},"_3-锁竞争为什么不一定直接表现为高-cpu","3. 锁竞争为什么不一定直接表现为高 CPU",[18,24722,24723],{},"普通阻塞锁竞争中的线程可能进入等待状态，CPU 不一定高；但以下情况可能提高 CPU：",[159,24725,24726,24729,24732,24735],{},[162,24727,24728],{},"自旋锁或轻量级锁长时间竞争；",[162,24730,24731],{},"大量线程频繁唤醒又重新阻塞；",[162,24733,24734],{},"临界区过小但竞争极其频繁；",[162,24736,24737],{},"公平竞争和线程调度造成大量切换。",[18,24739,24740,24741,33],{},"因此要结合 JFR 同步事件、线程状态、上下文切换和吞吐判断，而不是只看线程栈里是否出现 ",[22,24742,24743],{},"BLOCKED",[18,24745,24746],{},"JFR 中常用的同步与等待事件包括：",[299,24748,24749,24759],{},[302,24750,24751],{},[305,24752,24753,24756],{},[308,24754,24755],{},"事件",[308,24757,24758],{},"主要表示什么",[315,24760,24761,24774,24788],{},[305,24762,24763,24768],{},[320,24764,24765],{},[22,24766,24767],{},"jdk.JavaMonitorEnter",[320,24769,24770,24771,24773],{},"线程进入 ",[22,24772,2172],{}," 监视器时发生了阻塞，可用于分析锁竞争",[305,24775,24776,24781],{},[320,24777,24778],{},[22,24779,24780],{},"jdk.JavaMonitorWait",[320,24782,24783,24784,24787],{},"线程执行 ",[22,24785,24786],{},"Object.wait()"," 后等待监视器通知或超时",[305,24789,24790,24795],{},[320,24791,24792],{},[22,24793,24794],{},"jdk.ThreadPark",[320,24796,24797,24798,24801],{},"线程通过 ",[22,24799,24800],{},"LockSupport.park()"," 等机制进入等待，AQS 锁和并发组件经常使用它",[18,24803,24804],{},"这些事件是否记录以及记录多短的等待，取决于所使用的 JFR 配置和事件阈值。分析时要关注累计等待时间、事件次数、关联线程和调用栈，不能把正常的短暂等待都判断为性能问题。",[14,24806,24808],{"id":24807},"九区分-cpu-使用率饱和度和业务压力","九、区分 CPU 使用率、饱和度和业务压力",[18,24810,24811],{},"CPU 使用率高本身不一定是故障。如果系统在高流量下：",[159,24813,24814,24817,24820,24823,24826],{},[162,24815,24816],{},"吞吐正常；",[162,24818,24819],{},"P99 延迟可接受；",[162,24821,24822],{},"错误率稳定；",[162,24824,24825],{},"没有明显排队和超时；",[162,24827,24828],{},"CPU 仍保留安全余量；",[18,24830,24831],{},"那么它可能只是有效使用计算资源。",[18,24833,24834],{},"真正需要警惕的是：",[159,24836,24837,24840,24843,24846,24849,24852,24855],{},[162,24838,24839],{},"CPU 长时间接近上限；",[162,24841,24842],{},"Run Queue 持续增长；",[162,24844,24845],{},"延迟和超时显著上升；",[162,24847,24848],{},"吞吐不再增加；",[162,24850,24851],{},"容器 CPU 限流持续加重；",[162,24853,24854],{},"单实例负载明显倾斜；",[162,24856,24857],{},"流量下降后 CPU 无法恢复。",[18,24859,24860],{},"处理时应把 CPU 与 QPS、任务速率、延迟、错误率和实例数放在同一时间线上分析。",[18,24862,24863,24864,24866,24867,24870],{},"**Run Queue（运行队列）**可以理解为已经具备运行条件、正在使用 CPU 或正在等待 CPU 时间片的任务数量。在 ",[22,24865,23613],{}," 中通常观察 ",[22,24868,24869],{},"r"," 列。它不是线程池任务队列，也不包含普通的休眠线程。",[18,24872,24873,24874,24876],{},"判断 Run Queue 是否异常要结合 CPU 核数：短时波动很常见；如果 ",[22,24875,24869],{}," 长时间明显高于可用 CPU 核数，同时 CPU 接近饱和、延迟上升，说明可运行任务持续排队，系统已经出现 CPU 调度压力。",[14,24878,24880],{"id":24879},"十常见场景与证据对应","十、常见场景与证据对应",[299,24882,24883,24895],{},[302,24884,24885],{},[305,24886,24887,24889,24892],{},[308,24888,22910],{},[308,24890,24891],{},"重点证据",[308,24893,24894],{},"常见根因",[315,24896,24897,24908,24919,24930,24944,24959,24973],{},[305,24898,24899,24902,24905],{},[320,24900,24901],{},"单个线程稳定占满一个核",[320,24903,24904],{},"热点 TID + 多份线程栈",[320,24906,24907],{},"死循环、重试、单线程热点计算",[305,24909,24910,24913,24916],{},[320,24911,24912],{},"多个业务线程共同升高",[320,24914,24915],{},"JFR、CPU 火焰图、请求量",[320,24917,24918],{},"流量增长、序列化、压缩、算法热点",[305,24920,24921,24924,24927],{},[320,24922,24923],{},"GC 线程 CPU 高",[320,24925,24926],{},"GC 日志、JFR、分配火焰图",[320,24928,24929],{},"分配速率过高、存活对象过多",[305,24931,24932,24935,24941],{},[320,24933,24934],{},"线程数与切换次数激增",[320,24936,24937,24938,24940],{},"线程数、",[22,24939,24660],{},"、线程栈",[320,24942,24943],{},"线程池失控、任务堆积、阻塞补偿过度",[305,24945,24946,24949,24956],{},[320,24947,24948],{},"机器 CPU 高但 JVM CPU 低",[320,24950,24951,24952,53,24954],{},"进程 CPU、",[22,24953,23590],{},[22,24955,23603],{},[320,24957,24958],{},"其他进程、内核、中断或宿主机问题",[305,24960,24961,24964,24970],{},[320,24962,24963],{},"CPU 未满但延迟很高",[320,24965,24966,24967,24969],{},"cgroup ",[22,24968,23785],{},"、容器 limit",[320,24971,24972],{},"容器 CPU 限流或配额过低",[305,24974,24975,24978,24981],{},[320,24976,24977],{},"CPU 高且异常日志暴增",[320,24979,24980],{},"异常指标、日志速率、火焰图",[320,24982,24983],{},"异常堆栈生成、重复日志和失败重试",[14,24985,24987],{"id":24986},"十一修复后的验证","十一、修复后的验证",[18,24989,24990],{},"修复完成后至少验证：",[2695,24992,24993,24996,24999,25002,25005,25008,25011,25014],{},[162,24994,24995],{},"相同或更高流量下 CPU 是否恢复到合理范围；",[162,24997,24998],{},"吞吐、平均延迟和 P99 延迟是否改善；",[162,25000,25001],{},"错误率和超时率是否恢复；",[162,25003,25004],{},"GC 频率、分配速率和 GC 线程 CPU 是否正常；",[162,25006,25007],{},"线程数和上下文切换是否稳定；",[162,25009,25010],{},"容器 CPU 限流是否下降；",[162,25012,25013],{},"降峰后 CPU 是否能够及时回落；",[162,25015,25016],{},"是否出现把 CPU 问题转移成内存、队列积压或下游压力的问题。",[18,25018,25019],{},"不能只看某个热点方法消失就宣布修复。优化可能降低单次计算成本，也可能只是把任务积压到了队列或下游系统。",[14,25021,25023],{"id":25022},"十二预防与监控","十二、预防与监控",[18,25025,25026],{},"建议建立以下监控：",[159,25028,25029,25032,25035,25038,25041,25044,25047,25050,25053],{},[162,25030,25031],{},"机器、容器和 JVM 进程 CPU；",[162,25033,25034],{},"用户态、内核态、I\u002FO wait 和 steal time；",[162,25036,25037],{},"CPU request、limit 与容器 CPU 限流；",[162,25039,25040],{},"线程总数、RUNNABLE 线程数和上下文切换；",[162,25042,25043],{},"GC 线程 CPU、停顿、频率和分配速率；",[162,25045,25046],{},"接口 QPS、任务速率、延迟和错误率；",[162,25048,25049],{},"消息积压、线程池活跃线程、队列长度和拒绝次数；",[162,25051,25052],{},"异常数量和日志写入速率；",[162,25054,25055],{},"单实例负载分布。",[18,25057,25058,25059,6265,25062,25065,25066,25068,25069,24398],{},"对于偶发且持续时间很短的问题，可以考虑常态化开启低开销的 JFR 连续记录。连续记录是让 JFR 在后台长期以较低开销采集事件，并通过 ",[22,25060,25061],{},"maxage",[22,25063,25064],{},"maxsize"," 维护一个循环缓冲区；旧数据会被自动覆盖，告警发生后再导出最近数小时的运行现场。长期记录通常使用偏低开销的 ",[22,25067,24390],{}," 配置，需要更细采样时再短时开启 ",[22,25070,24394],{},[14,25072,25074],{"id":25073},"十三常见误区","十三、常见误区",[473,25076,25078],{"id":25077},"_1-cpu-高就立即扩容","1. CPU 高就立即扩容",[18,25080,25081],{},"扩容可以缓解容量不足，但无法根治死循环、重试风暴或热点数据倾斜。异常逻辑可能在每个新实例上再次出现。",[473,25083,25085],{"id":25084},"_2-只抓一份线程栈","2. 只抓一份线程栈",[18,25087,25088],{},"单个快照不能证明某个线程持续占用 CPU，应连续采样并与线程 CPU 数据关联。",[473,25090,25092],{"id":25091},"_3-只看线程状态","3. 只看线程状态",[18,25094,25095,25097,25098,25100],{},[22,25096,24090],{}," 不等于线程一定正在消耗大量 CPU。Java 的 ",[22,25099,24090],{}," 还可能包含正在执行某些原生或网络操作的线程，需要结合 TID CPU、连续线程栈和采样分析。",[473,25102,25104],{"id":25103},"_4-看到-gc-线程就直接调大堆","4. 看到 GC 线程就直接调大堆",[18,25106,25107],{},"堆变大可能降低 GC 频率，也可能增加停顿和资源占用。应先确认分配热点、存活对象和容量模型。",[473,25109,25111],{"id":25110},"_5-在线上长时间开启重型分析","5. 在线上长时间开启重型分析",[18,25113,25114],{},"诊断工具本身也有开销。应优先使用低开销采样，限定持续时间和输出大小，并评估目标实例当前承压能力。",[14,25116,25118],{"id":25117},"十四推荐排查顺序","十四、推荐排查顺序",[18,25120,25121],{},"可以记成下面这条主线：",[144,25123,25126],{"className":25124,"code":25125,"language":149,"meta":150},[147],"止损并保留现场\n    ↓\n确认机器、容器、进程哪一层 CPU 高\n    ↓\n确认是否为 Java 进程\n    ↓\n找到高 CPU TID，并与线程栈 nid 对应\n    ↓\n连续线程栈仍不明确时，使用 JFR 或 async-profiler\n    ↓\n结合 GC、线程数、锁、流量和容器限流定位根因\n    ↓\n修复后用真实负载验证，并补齐监控和容量保护\n",[22,25127,25125],{"__ignoreMap":150},[14,25129,5803],{"id":5803},[159,25131,25132,25139,25146,25153,25160,25167,25174,25181,25188],{},[162,25133,25134],{},[119,25135,25138],{"href":25136,"rel":25137},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F17\u002Ftroubleshoot\u002Ftroubleshoot-process-hangs-and-loops.html",[2080],"Oracle：Troubleshoot Process Hangs and Loops",[162,25140,25141],{},[119,25142,25145],{"href":25143,"rel":25144},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F26\u002Ftroubleshoot\u002Ftroubleshoot-performance-issues-using-jfr.html",[2080],"Oracle：Troubleshoot Performance Issues Using Flight Recorder",[162,25147,25148],{},[119,25149,25152],{"href":25150,"rel":25151},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F26\u002Fjfapi\u002Fflight-recorder-configurations.html",[2080],"Oracle：Flight Recorder Configurations",[162,25154,25155],{},[119,25156,25159],{"href":25157,"rel":25158},"https:\u002F\u002Fdocs.oracle.com\u002Fen\u002Fjava\u002Fjavase\u002F22\u002Fdocs\u002Fspecs\u002Fman\u002Fjcmd.html",[2080],"Oracle：jcmd Command",[162,25161,25162],{},[119,25163,25166],{"href":25164,"rel":25165},"https:\u002F\u002Fgithub.com\u002Fasync-profiler\u002Fasync-profiler",[2080],"async-profiler 官方项目",[162,25168,25169],{},[119,25170,25173],{"href":25171,"rel":25172},"https:\u002F\u002Fwww.kernel.org\u002Fdoc\u002Fhtml\u002Flatest\u002Fadmin-guide\u002Fcgroup-v2.html",[2080],"Linux Kernel：Control Group v2",[162,25175,25176],{},[119,25177,25180],{"href":25178,"rel":25179},"https:\u002F\u002Fman7.org\u002Flinux\u002Fman-pages\u002Fman1\u002Fmpstat.1.html",[2080],"Linux man-pages：mpstat",[162,25182,25183],{},[119,25184,25187],{"href":25185,"rel":25186},"https:\u002F\u002Fman7.org\u002Flinux\u002Fman-pages\u002Fman1\u002Fpidstat.1.html",[2080],"Linux man-pages：pidstat",[162,25189,25190],{},[119,25191,25194],{"href":25192,"rel":25193},"https:\u002F\u002Fman7.org\u002Flinux\u002Fman-pages\u002Fman8\u002Fvmstat.8.html",[2080],"Linux man-pages：vmstat",[2111,25196,25197],{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}",{"title":150,"searchDepth":251,"depth":251,"links":25199},[25200,25201,25202,25205,25206,25207,25212,25217,25224,25228,25229,25234,25235,25236,25237,25238,25245,25246],{"id":16,"depth":251,"text":16},{"id":115,"depth":251,"text":115},{"id":132,"depth":251,"text":132,"children":25203},[25204],{"id":23377,"depth":260,"text":23378},{"id":23463,"depth":251,"text":23464},{"id":23522,"depth":251,"text":23523},{"id":23567,"depth":251,"text":23568,"children":25208},[25209,25210,25211],{"id":23571,"depth":260,"text":23572},{"id":23758,"depth":260,"text":23759},{"id":23789,"depth":260,"text":23790},{"id":23860,"depth":251,"text":23861,"children":25213},[25214,25215,25216],{"id":23864,"depth":260,"text":23865},{"id":23964,"depth":260,"text":23965},{"id":24080,"depth":260,"text":24081},{"id":24108,"depth":251,"text":24109,"children":25218},[25219,25220,25221,25222,25223],{"id":24112,"depth":260,"text":24113},{"id":24179,"depth":260,"text":24180},{"id":24221,"depth":260,"text":24222},{"id":24237,"depth":260,"text":24238},{"id":24264,"depth":260,"text":24265},{"id":24288,"depth":251,"text":24289,"children":25225},[25226,25227],{"id":24315,"depth":260,"text":24316},{"id":24401,"depth":260,"text":24402},{"id":24467,"depth":251,"text":24468},{"id":24584,"depth":251,"text":24585,"children":25230},[25231,25232,25233],{"id":24588,"depth":260,"text":24589},{"id":24705,"depth":260,"text":24706},{"id":24719,"depth":260,"text":24720},{"id":24807,"depth":251,"text":24808},{"id":24879,"depth":251,"text":24880},{"id":24986,"depth":251,"text":24987},{"id":25022,"depth":251,"text":25023},{"id":25073,"depth":251,"text":25074,"children":25239},[25240,25241,25242,25243,25244],{"id":25077,"depth":260,"text":25078},{"id":25084,"depth":260,"text":25085},{"id":25091,"depth":260,"text":25092},{"id":25103,"depth":260,"text":25104},{"id":25110,"depth":260,"text":25111},{"id":25117,"depth":251,"text":25118},{"id":5803,"depth":251,"text":5803},"wiki:java:java-high-cpu-troubleshooting","从线上止损、确认 CPU 消耗主体、定位高 CPU 线程、关联 Java 栈与火焰图，到死循环、GC、锁竞争、线程膨胀和热点计算分析，系统讲解 Java 服务 CPU 飙高的排查方法。",{},82,"\u002Fjava\u002Fjava-high-cpu-troubleshooting",{"title":23318,"description":25248},"java\u002Fjava-high-cpu-troubleshooting","2026-07-29","rR74pFuh7iWZlh88nnawzfp43Tnwi0qpY3xk0tzgWoI",{"id":25257,"title":25258,"body":25259,"commentId":27504,"description":27505,"difficulty":3667,"draft":2148,"extension":2149,"meta":27506,"navigation":756,"order":27507,"path":27508,"section":2153,"seo":27509,"stem":27510,"updated":27511,"__hash__":27512},"java\u002Fjava\u002Fhow-to-design-a-configuration-center.md","如何设计一个配置中心？",{"type":7,"value":25260,"toc":27442},[25261,25264,25266,25269,25272,25275,25296,25299,25301,25306,25308,25317,25319,25323,25326,25346,25349,25369,25372,25389,25396,25400,25403,25409,25412,25477,25480,25484,25505,25508,25512,25548,25551,25554,25558,25562,25565,25588,25591,25595,25598,25618,25621,25625,25628,25634,25638,25641,25644,25661,25665,25675,25681,25688,25692,25695,25701,25704,25708,25711,25717,25720,25723,25727,25730,25734,25737,25743,25746,25750,25753,25756,25760,25763,25766,25780,25784,25787,25793,25796,25800,25803,25817,25820,25824,25827,25831,25834,25840,25844,25847,25865,25868,25872,25875,25970,25979,25982,26012,26015,26019,26022,26028,26032,26035,26041,26044,26048,26094,26098,26101,26118,26121,26125,26128,26134,26137,26141,26150,26155,26161,26164,26170,26174,26265,26273,26277,26282,26407,26410,26416,26428,26437,26446,26463,26467,26474,26480,26483,26501,26504,26510,26516,26520,26523,26614,26623,26626,26632,26637,26640,26646,26652,26664,26671,26706,26718,26722,26725,26766,26769,26775,26778,26782,26785,26802,26805,26811,26817,26821,26824,26830,26833,26839,26845,26849,26852,26869,26872,26878,26881,26884,26888,26892,26895,26920,26923,26926,26930,26933,26939,26942,26946,26949,26963,26966,26969,26973,26976,26996,26999,27005,27008,27014,27017,27040,27043,27047,27050,27053,27082,27085,27088,27092,27095,27127,27130,27136,27139,27143,27229,27232,27236,27240,27243,27247,27250,27254,27257,27261,27264,27268,27271,27275,27278,27284,27287,27291,27294,27298,27318,27322,27342,27346,27369,27372,27374,27439],[10,25262,25258],{"id":25263},"如何设计一个配置中心",[14,25265,16],{"id":16},[18,25267,25268],{},"设计配置中心时，我会先把系统分成管理面和运行时分发面。管理面负责配置编辑、校验、审批、发布、灰度、回滚、权限和审计；分发面负责客户端查询、变更通知、版本校准和本地容灾。配置按照环境、租户、应用、命名空间和配置标识进行隔离，每次发布都生成不可变版本，数据库保存权威的已发布快照和历史记录。",[18,25270,25271],{},"发布时不能直接覆盖线上值，而应经过“编辑草稿、格式与规则校验、审批、事务生成新版本、写变更事件、通知客户端”的流程。客户端启动时主动拉取完整快照，运行期间通过长轮询或长连接监听变化；服务端通常只通知配置标识和版本，客户端再拉取内容。为了防止通知丢失，客户端还要定时校准版本。",[18,25273,25274],{},"客户端获取新配置后，应先校验版本、格式和完整性，再原子替换内存快照并落盘保存最后一次正确版本，然后通知业务监听器。配置中心不可用时，已运行实例继续使用内存配置，新实例优先读取本地快照；如果关键配置既无法远程获取也没有本地快照，应按业务要求失败退出，而不是用不明确的默认值启动。",[18,25276,25277,25278,25280,25281,25284,25285,25288,25289,25291,25292,25295],{},"在 Spring 应用中，配置中心 SDK 获取新版本只是第一步。集成层还需要先更新 Spring ",[22,25279,20643],{}," 中对应的 ",[22,25282,25283],{},"PropertySource","，再发布变更事件，使 ",[22,25286,25287],{},"@ConfigurationProperties"," 重新绑定；对于只在 Bean 创建时注入的 ",[22,25290,19830],{}," 或需要整体重建的组件，则应通过 ",[22,25293,25294],{},"@RefreshScope","、显式监听器或不可变配置对象原子替换来应用新值。",[18,25297,25298],{},"高可用方面，配置读取和通知服务应无状态多实例部署，权威存储使用高可用数据库或一致性存储；服务端和客户端都要有缓存，通知链路需要重试和定时校准兜底。平台还必须具备灰度发布、版本回滚、RBAC、敏感信息保护、操作审计、客户端版本观测、容量限制和发布成功率监控。",[18,25300,103],{},[105,25302,25303],{},[18,25304,25305],{},"配置中心的核心不是保存 Key-Value，而是让正确的配置经过可审计的发布流程，以版本化、可恢复的方式安全送达客户端。",[14,25307,115],{"id":115},[18,25309,25310],{},[119,25311,25314],{"href":25312,"target":122,"rel":25313},"\u002Fimages\u002Fwiki\u002Fjava\u002Fhow-to-design-a-configuration-center.svg",[124,125],[127,25315],{"src":25312,"alt":25316},"配置中心管理面、发布链路、运行时分发、客户端容灾和安全治理总览",[14,25318,132],{"id":132},[14,25320,25322],{"id":25321},"一先明确配置中心要解决什么","一、先明确配置中心要解决什么",[18,25324,25325],{},"配置中心主要解决以下问题：",[159,25327,25328,25331,25334,25337,25340,25343],{},[162,25329,25330],{},"不重新构建和部署应用，也能修改运行参数；",[162,25332,25333],{},"集中管理不同环境、租户、集群和应用的配置；",[162,25335,25336],{},"让大量应用实例及时感知配置变化；",[162,25338,25339],{},"知道谁在什么时间发布了什么内容；",[162,25341,25342],{},"支持灰度验证、版本回滚和故障恢复；",[162,25344,25345],{},"在配置中心或网络异常时，应用仍能使用最后一次正确配置。",[18,25347,25348],{},"适合放入配置中心的内容包括：",[159,25350,25351,25354,25357,25360,25363,25366],{},[162,25352,25353],{},"功能开关；",[162,25355,25356],{},"超时、重试次数和线程池参数；",[162,25358,25359],{},"业务规则和路由规则；",[162,25361,25362],{},"限流阈值；",[162,25364,25365],{},"非敏感的外部服务地址；",[162,25367,25368],{},"可以安全热更新的运行参数。",[18,25370,25371],{},"以下内容不宜直接混在普通配置中：",[159,25373,25374,25377,25380,25383,25386],{},[162,25375,25376],{},"大文件、图片和安装包；",[162,25378,25379],{},"高频变化的业务状态；",[162,25381,25382],{},"需要复杂查询的业务数据；",[162,25384,25385],{},"密码、私钥、访问令牌等完整生命周期的密钥数据；",[162,25387,25388],{},"修改后必须重启或迁移数据才能生效的参数。",[18,25390,25391,25392,25395],{},"密钥可以在配置中保存引用，例如 ",[22,25393,25394],{},"secret:\u002F\u002Fpayment\u002Fprod\u002Fdb-password","，真实密文由专门的密钥管理系统保存、轮换和审计。配置中心不应被扩展成文档存储、业务数据库或密钥中心。",[14,25397,25399],{"id":25398},"二先做领域模型而不是直接建一张-key-value-表","二、先做领域模型，而不是直接建一张 Key-Value 表",[18,25401,25402],{},"一个通用配置身份可以设计为：",[144,25404,25407],{"className":25405,"code":25406,"language":149,"meta":150},[147],"tenant\n  └── environment\n        └── application\n              └── namespace\n                    └── config key \u002F document\n",[22,25408,25406],{"__ignoreMap":150},[18,25410,25411],{},"各维度的作用：",[299,25413,25414,25424],{},[302,25415,25416],{},[305,25417,25418,25421],{},[308,25419,25420],{},"维度",[308,25422,25423],{},"作用",[315,25425,25426,25436,25446,25455,25465],{},[305,25427,25428,25433],{},[320,25429,25430],{},[22,25431,25432],{},"tenant",[320,25434,25435],{},"隔离不同租户或组织",[305,25437,25438,25443],{},[320,25439,25440],{},[22,25441,25442],{},"environment",[320,25444,25445],{},"隔离开发、测试、预发和生产环境",[305,25447,25448,25452],{},[320,25449,25450],{},[22,25451,21622],{},[320,25453,25454],{},"标识配置归属的应用",[305,25456,25457,25462],{},[320,25458,25459],{},[22,25460,25461],{},"namespace",[320,25463,25464],{},"按模块或用途组织配置，并支持共享配置",[305,25466,25467,25474],{},[320,25468,25469,6265,25471],{},[22,25470,11314],{},[22,25472,25473],{},"dataId",[320,25475,25476],{},"唯一定位一个配置项或配置文档",[18,25478,25479],{},"配置内容有两种常见模型。",[473,25481,25483],{"id":25482},"_1-独立-key-value","1. 独立 Key-Value",[144,25485,25487],{"className":4755,"code":25486,"language":4757,"meta":150,"style":150},"payment.timeout=3000\npayment.retry-count=3\n",[22,25488,25489,25497],{"__ignoreMap":150},[203,25490,25491,25494],{"class":205,"line":206},[203,25492,25493],{"class":209},"payment.timeout",[203,25495,25496],{"class":219},"=3000\n",[203,25498,25499,25502],{"class":205,"line":251},[203,25500,25501],{"class":209},"payment.retry-count",[203,25503,25504],{"class":219},"=3\n",[18,25506,25507],{},"优点是可以单独修改和审计某个配置项；缺点是一次业务变更涉及多个 Key 时，需要额外保证它们作为一个整体发布。",[473,25509,25511],{"id":25510},"_2-配置文档","2. 配置文档",[144,25513,25517],{"className":25514,"code":25515,"language":25516,"meta":150,"style":150},"language-yaml shiki shiki-themes github-light github-dark","payment:\n  timeout: 3000\n  retry-count: 3\n","yaml",[22,25518,25519,25527,25538],{"__ignoreMap":150},[203,25520,25521,25524],{"class":205,"line":206},[203,25522,25523],{"class":3309},"payment",[203,25525,25526],{"class":219},":\n",[203,25528,25529,25532,25535],{"class":205,"line":251},[203,25530,25531],{"class":3309},"  timeout",[203,25533,25534],{"class":219},": ",[203,25536,25537],{"class":718},"3000\n",[203,25539,25540,25543,25545],{"class":205,"line":260},[203,25541,25542],{"class":3309},"  retry-count",[203,25544,25534],{"class":219},[203,25546,25547],{"class":718},"3\n",[18,25549,25550],{},"整个 YAML、JSON 或 Properties 文档作为一个发布单元，天然适合原子替换一组相关配置，但文档过大时会增加传输、对比和解析成本。",[18,25552,25553],{},"工程中可以同时支持两种模型，但必须明确最小发布单元。客户端不能先看到新超时、后看到旧重试次数，形成不存在于任何正式版本中的中间状态。",[14,25555,25557],{"id":25556},"三管理面与分发面要分开","三、管理面与分发面要分开",[473,25559,25561],{"id":25560},"_1-管理面","1. 管理面",[18,25563,25564],{},"管理面面向开发、测试和运维人员，负责：",[159,25566,25567,25570,25573,25576,25579,25582,25585],{},[162,25568,25569],{},"应用、环境和命名空间管理；",[162,25571,25572],{},"配置编辑与差异对比；",[162,25574,25575],{},"格式、类型和业务规则校验；",[162,25577,25578],{},"审批与发布；",[162,25580,25581],{},"灰度规则；",[162,25583,25584],{},"历史版本和回滚；",[162,25586,25587],{},"权限与操作审计。",[18,25589,25590],{},"管理接口的 QPS 通常不高，但对权限、数据正确性和审计完整性要求很高。",[473,25592,25594],{"id":25593},"_2-运行时分发面","2. 运行时分发面",[18,25596,25597],{},"分发面面向应用 SDK，负责：",[159,25599,25600,25603,25606,25609,25612,25615],{},[162,25601,25602],{},"查询当前已发布配置；",[162,25604,25605],{},"监听配置变化；",[162,25607,25608],{},"返回版本和校验摘要；",[162,25610,25611],{},"维护服务端缓存；",[162,25613,25614],{},"记录客户端正在使用的版本；",[162,25616,25617],{},"在大量客户端之间高效分发变更。",[18,25619,25620],{},"分发面的特点是读多写少、连接数量大、发布时可能出现瞬时通知扇出。因此应与控制台和管理接口隔离资源，避免一次批量查询或错误发布拖垮所有客户端的配置读取。",[14,25622,25624],{"id":25623},"四配置发布链路怎么设计","四、配置发布链路怎么设计",[18,25626,25627],{},"推荐发布流程：",[144,25629,25632],{"className":25630,"code":25631,"language":149,"meta":150},[147],"编辑草稿\n→ 语法和规则校验\n→ 查看差异\n→ 审批\n→ 生成不可变版本\n→ 更新当前正式版本指针\n→ 写入变更事件\n→ 通知分发节点和客户端\n",[22,25633,25631],{"__ignoreMap":150},[473,25635,25637],{"id":25636},"_1-编辑与发布分离","1. 编辑与发布分离",[18,25639,25640],{},"编辑操作只修改草稿，不能直接影响线上实例。发布才会生成正式版本。",[18,25642,25643],{},"这样可以支持：",[159,25645,25646,25649,25652,25655,25658],{},[162,25647,25648],{},"多次修改后一次发布；",[162,25650,25651],{},"发布前 Diff；",[162,25653,25654],{},"双人复核或审批；",[162,25656,25657],{},"定时发布；",[162,25659,25660],{},"取消未完成的修改。",[473,25662,25664],{"id":25663},"_2-发布必须有版本","2. 发布必须有版本",[18,25666,25667,25668,25671,25672,431],{},"每次正式发布生成单调递增的 ",[22,25669,25670],{},"revision"," 或唯一 ",[22,25673,25674],{},"releaseId",[144,25676,25679],{"className":25677,"code":25678,"language":149,"meta":150},[147],"revision 101 → timeout=3000\nrevision 102 → timeout=5000\nrevision 103 → timeout=4000\n",[22,25680,25678],{"__ignoreMap":150},[18,25682,25683,25684,25687],{},"回滚到 ",[22,25685,25686],{},"revision 101"," 时，不应删除 102 和 103，也不应把版本号改回 101，而是以 101 的内容生成新版本 104。这样历史始终完整，客户端版本也只向前推进。",[473,25689,25691],{"id":25690},"_3-使用乐观锁防止覆盖","3. 使用乐观锁防止覆盖",[18,25693,25694],{},"两个用户可能同时基于版本 101 编辑。发布接口应携带期望版本：",[144,25696,25699],{"className":25697,"code":25698,"language":149,"meta":150},[147],"publish(\n    configId,\n    expectedRevision = 101,\n    newContent\n)\n",[22,25700,25698],{"__ignoreMap":150},[18,25702,25703],{},"如果当前版本已经是 102，发布应失败并提示重新对比，而不是静默覆盖其他人的修改。这相当于配置发布场景中的 CAS。",[473,25705,25707],{"id":25706},"_4-配置与变更事件要可靠衔接","4. 配置与变更事件要可靠衔接",[18,25709,25710],{},"如果数据库写入成功，但通知事件发送失败，客户端可能无法及时感知变化。可以在同一个数据库事务中写入：",[144,25712,25715],{"className":25713,"code":25714,"language":149,"meta":150},[147],"正式版本\n+ 发布历史\n+ Outbox 变更事件\n",[22,25716,25714],{"__ignoreMap":150},[18,25718,25719],{},"后台任务再可靠投递 Outbox 事件。即使消息系统短暂故障，事件仍可重试。客户端的定时版本校准是最后一道兜底，因此系统正确性不能只依赖一次通知。",[18,25721,25722],{},"Outbox 是“事务发件箱”模式：业务数据和待发送事件先写入同一个数据库事务，事务提交后再由后台任务把事件投递到消息系统。它解决的是“数据库已经提交，但消息没发出去”的双写不一致问题。",[14,25724,25726],{"id":25725},"五为什么通常采用通知变化再主动拉取","五、为什么通常采用“通知变化，再主动拉取”",[18,25728,25729],{},"客户端同步配置有三种方式。",[473,25731,25733],{"id":25732},"_1-定时轮询","1. 定时轮询",[18,25735,25736],{},"客户端每隔固定时间请求服务端：",[144,25738,25741],{"className":25739,"code":25740,"language":149,"meta":150},[147],"当前版本是 101，有更新吗？\n",[22,25742,25740],{"__ignoreMap":150},[18,25744,25745],{},"优点是简单、天然可以修复通知丢失；缺点是实时性与请求量难以兼顾。轮询越频繁，没有发生配置变更却仍然访问服务端的请求越多；间隔越长，配置生效越慢。",[473,25747,25749],{"id":25748},"_2-长轮询","2. 长轮询",[18,25751,25752],{},"客户端发起请求，版本未变化时服务端暂不返回；配置变化或请求超时后再响应。客户端收到响应后立即重新发起下一次监听。",[18,25754,25755],{},"它可以减少无效轮询，并保持较好的变更实时性。Apollo 的经典实现就使用 HTTP Long Polling。",[473,25757,25759],{"id":25758},"_3-长连接推送","3. 长连接推送",[18,25761,25762],{},"客户端与服务端维持 gRPC、WebSocket 或自定义 TCP 长连接。配置变化时，服务端沿连接发送通知。当前 Nacos SDK 的监听采用长连接机制。",[18,25764,25765],{},"长连接实时性更好，但需要处理：",[159,25767,25768,25771,25774,25777],{},[162,25769,25770],{},"连接保活和重连；",[162,25772,25773],{},"服务端连接容量；",[162,25775,25776],{},"网络抖动；",[162,25778,25779],{},"节点迁移。",[473,25781,25783],{"id":25782},"_4-推荐组合","4. 推荐组合",[18,25785,25786],{},"无论使用长轮询还是长连接，都推荐：",[144,25788,25791],{"className":25789,"code":25790,"language":149,"meta":150},[147],"服务端通知：configId + newRevision\n客户端查询：获取 newRevision 对应的完整内容\n定时校准：周期性比较服务端与本地 revision\n",[22,25792,25790],{"__ignoreMap":150},[18,25794,25795],{},"通知只告诉客户端“哪里变了”，不携带大段配置内容。这样能降低扇出流量，也让所有内容读取统一走查询、鉴权、灰度匹配和校验链路。",[14,25797,25799],{"id":25798},"六如何保证客户端最终拿到正确版本","六、如何保证客户端最终拿到正确版本",[18,25801,25802],{},"配置中心通常不是所有链路都强一致。",[159,25804,25805,25808,25811,25814],{},[162,25806,25807],{},"发布事务需要保证版本、历史和当前正式指针原子更新；",[162,25809,25810],{},"配置查询要能读取确定的已发布版本；",[162,25812,25813],{},"变更通知可以最终一致，因为网络和客户端状态不可控；",[162,25815,25816],{},"客户端应用配置应保证单个发布单元原子替换。",[18,25818,25819],{},"客户端需要遵守以下规则：",[473,25821,25823],{"id":25822},"_1-版本只能前进","1. 版本只能前进",[18,25825,25826],{},"如果本地已经应用版本 103，后来收到延迟到达的版本 102 通知，应直接丢弃，不能回退。",[473,25828,25830],{"id":25829},"_2-通知必须幂等","2. 通知必须幂等",[18,25832,25833],{},"同一个版本被重复通知，不应重复执行有副作用的刷新逻辑。客户端可以比较：",[144,25835,25838],{"className":25836,"code":25837,"language":149,"meta":150},[147],"newRevision \u003C= localRevision\n→ 忽略\n",[22,25839,25837],{"__ignoreMap":150},[473,25841,25843],{"id":25842},"_3-内容必须校验","3. 内容必须校验",[18,25845,25846],{},"服务端响应通常携带：",[159,25848,25849,25853,25856,25859,25862],{},[162,25850,25851,2365],{},[22,25852,25670],{},[162,25854,25855],{},"内容类型；",[162,25857,25858],{},"内容摘要，例如 SHA-256；",[162,25860,25861],{},"修改时间；",[162,25863,25864],{},"灰度或正式版本标识。",[18,25866,25867],{},"客户端应先验证内容摘要和格式，再应用新配置。校验失败时继续使用旧版本并告警，不能用半解析数据覆盖当前配置。",[473,25869,25871],{"id":25870},"_4-原子替换快照","4. 原子替换快照",[18,25873,25874],{},"推荐把相关配置解析成不可变对象：",[144,25876,25878],{"className":197,"code":25877,"language":199,"meta":150,"style":150},"final class PaymentConfig {\n    private final Duration timeout;\n    private final int retryCount;\n\n    PaymentConfig(Duration timeout, int retryCount) {\n        this.timeout = timeout;\n        this.retryCount = retryCount;\n    }\n}\n",[22,25879,25880,25892,25901,25913,25917,25938,25951,25962,25966],{"__ignoreMap":150},[203,25881,25882,25885,25887,25890],{"class":205,"line":206},[203,25883,25884],{"class":209},"final",[203,25886,1536],{"class":209},[203,25888,25889],{"class":215}," PaymentConfig",[203,25891,248],{"class":219},[203,25893,25894,25896,25898],{"class":205,"line":251},[203,25895,17969],{"class":209},[203,25897,17952],{"class":209},[203,25899,25900],{"class":219}," Duration timeout;\n",[203,25902,25903,25905,25907,25910],{"class":205,"line":260},[203,25904,17969],{"class":209},[203,25906,17952],{"class":209},[203,25908,25909],{"class":209}," int",[203,25911,25912],{"class":219}," retryCount;\n",[203,25914,25915],{"class":205,"line":278},[203,25916,757],{"emptyLinePlaceholder":756},[203,25918,25919,25922,25925,25928,25930,25933,25936],{"class":205,"line":285},[203,25920,25921],{"class":215},"    PaymentConfig",[203,25923,25924],{"class":219},"(Duration ",[203,25926,25927],{"class":271},"timeout",[203,25929,5179],{"class":219},[203,25931,25932],{"class":209},"int",[203,25934,25935],{"class":271}," retryCount",[203,25937,275],{"class":219},[203,25939,25940,25943,25946,25948],{"class":205,"line":541},[203,25941,25942],{"class":718},"        this",[203,25944,25945],{"class":219},".timeout ",[203,25947,495],{"class":209},[203,25949,25950],{"class":219}," timeout;\n",[203,25952,25953,25955,25958,25960],{"class":205,"line":549},[203,25954,25942],{"class":718},[203,25956,25957],{"class":219},".retryCount ",[203,25959,495],{"class":209},[203,25961,25912],{"class":219},[203,25963,25964],{"class":205,"line":787},[203,25965,751],{"class":219},[203,25967,25968],{"class":205,"line":799},[203,25969,288],{"class":219},[18,25971,25972,25973,25975,25976,25978],{},"不可变对象是指：对象完成构造后，其内部可观察状态不能再被修改。常见做法是把字段声明为 ",[22,25974,25884],{},"，不提供修改状态的 Setter，并对集合等可变成员进行防御性复制。需要注意，",[22,25977,25884],{}," 引用只表示引用不能改指向，并不自动保证它指向的对象不可变。",[18,25980,25981],{},"刷新时先完整构造新对象，最后一次性替换引用：",[144,25983,25985],{"className":197,"code":25984,"language":199,"meta":150,"style":150},"private volatile PaymentConfig currentConfig;\n\ncurrentConfig = newConfig;\n",[22,25986,25987,25998,26002],{"__ignoreMap":150},[203,25988,25989,25992,25995],{"class":205,"line":206},[203,25990,25991],{"class":209},"private",[203,25993,25994],{"class":209}," volatile",[203,25996,25997],{"class":219}," PaymentConfig currentConfig;\n",[203,25999,26000],{"class":205,"line":251},[203,26001,757],{"emptyLinePlaceholder":756},[203,26003,26004,26007,26009],{"class":205,"line":260},[203,26005,26006],{"class":219},"currentConfig ",[203,26008,495],{"class":209},[203,26010,26011],{"class":219}," newConfig;\n",[18,26013,26014],{},"读线程要么看到旧快照，要么看到新快照，不会读取到部分更新状态。",[14,26016,26018],{"id":26017},"七客户端-sdk-是可用性的关键","七、客户端 SDK 是可用性的关键",[18,26020,26021],{},"一个可靠的 SDK 至少包含：",[144,26023,26026],{"className":26024,"code":26025,"language":149,"meta":150},[147],"服务寻址\n身份认证\n启动拉取\n内存快照\n本地文件快照\n变更监听\n版本校验\n监听器调度\n定时校准\n指标上报\n",[22,26027,26025],{"__ignoreMap":150},[473,26029,26031],{"id":26030},"_1-启动流程","1. 启动流程",[18,26033,26034],{},"推荐顺序：",[144,26036,26039],{"className":26037,"code":26038,"language":149,"meta":150},[147],"读取本地最后正确快照\n→ 连接配置中心\n→ 拉取当前版本\n→ 校验并更新内存\n→ 建立监听\n→ 启动定时校准\n",[22,26040,26038],{"__ignoreMap":150},[18,26042,26043],{},"本地快照不是权威数据，而是服务端或网络故障时的容灾副本。",[473,26045,26047],{"id":26046},"_2-配置中心不可用时怎么办","2. 配置中心不可用时怎么办",[299,26049,26050,26060],{},[302,26051,26052],{},[305,26053,26054,26057],{},[308,26055,26056],{},"场景",[308,26058,26059],{},"推荐行为",[315,26061,26062,26070,26078,26086],{},[305,26063,26064,26067],{},[320,26065,26066],{},"已运行实例与服务端断开",[320,26068,26069],{},"继续使用当前内存快照，同时重连和告警",[305,26071,26072,26075],{},[320,26073,26074],{},"新实例远程拉取失败但存在本地快照",[320,26076,26077],{},"校验后使用最后正确快照，并标记为降级启动",[305,26079,26080,26083],{},[320,26081,26082],{},"关键配置远程拉取失败且没有本地快照",[320,26084,26085],{},"失败退出，避免用未知默认值运行",[305,26087,26088,26091],{},[320,26089,26090],{},"非关键配置缺失",[320,26092,26093],{},"可以使用经过明确登记的安全默认值",[473,26095,26097],{"id":26096},"_3-业务监听器必须隔离","3. 业务监听器必须隔离",[18,26099,26100],{},"配置变更回调不能直接阻塞 SDK 的网络线程。应通过独立、有界的执行器调用业务监听器，并设置：",[159,26102,26103,26106,26109,26112,26115],{},[162,26104,26105],{},"单次执行超时；",[162,26107,26108],{},"异常隔离；",[162,26110,26111],{},"慢监听器告警；",[162,26113,26114],{},"合并相同配置的连续更新；",[162,26116,26117],{},"必要时只保留最新版本。",[18,26119,26120],{},"如果版本 101、102、103 快速连续发布，而业务只关心当前状态，可以合并中间通知，直接应用 103；如果每个版本都代表必须执行的命令，则它本质上不是配置，应该使用消息系统。",[473,26122,26124],{"id":26123},"_4-spring-应用如何获取并应用最新配置","4. Spring 应用如何获取并应用最新配置",[18,26126,26127],{},"Spring 应用中的完整刷新链路可以分成两层：",[144,26129,26132],{"className":26130,"code":26131,"language":149,"meta":150},[147],"配置中心 SDK\n监听变更 → 拉取新版本 → 校验 revision、格式和摘要 → 更新本地快照\n\nSpring 集成层\n更新 PropertySource → 计算变化的 Key → 发布变更事件\n→ 重新绑定配置对象或重建指定 Bean → 业务代码读取新配置\n",[22,26133,26131],{"__ignoreMap":150},[18,26135,26136],{},"配置中心负责把新内容可靠送到应用，Spring 负责把这些内容接入自己的属性体系并更新使用配置的对象。新配置已经到达客户端，不等于业务 Bean 已经使用新值。",[6462,26138,26140],{"id":26139},"_41-先更新-environment-中的-propertysource","4.1 先更新 Environment 中的 PropertySource",[18,26142,26143,26144,26146,26147,26149],{},"Spring Boot 会把系统属性、环境变量、本地配置文件和远程配置等统一组织到 ",[22,26145,20643],{}," 中，每类来源对应一个或多个 ",[22,26148,25283],{},"，并按照优先级决定最终值。",[18,26151,26152,26153,431],{},"配置中心 SDK 拉取并校验新版本后，Spring 集成组件需要用新快照替换远程配置对应的 ",[22,26154,25283],{},[144,26156,26159],{"className":26157,"code":26158,"language":149,"meta":150},[147],"旧远程 PropertySource：revision 101\npayment.timeout = 3000\n\n新远程 PropertySource：revision 102\npayment.timeout = 5000\n",[22,26160,26158],{"__ignoreMap":150},[18,26162,26163],{},"替换必须发生在发布变更事件之前。否则监听器已经开始重新绑定，读取到的却仍然是旧值。",[18,26165,26166,26167,26169],{},"还要保留确定的配置源优先级。例如，设计上如果命令行参数应覆盖远程配置，那么刷新时不能把远程 ",[22,26168,25283],{}," 插到命令行参数之前，悄悄改变原有覆盖关系。",[6462,26171,26173],{"id":26172},"_42-environment-更新后不同使用方式的刷新行为不同","4.2 Environment 更新后，不同使用方式的刷新行为不同",[299,26175,26176,26189],{},[302,26177,26178],{},[305,26179,26180,26183,26186],{},[308,26181,26182],{},"使用方式",[308,26184,26185],{},"配置变化后的行为",[308,26187,26188],{},"推荐做法",[315,26190,26191,26209,26221,26240,26254],{},[305,26192,26193,26200,26206],{},[320,26194,26195,26196,26199],{},"每次通过 ",[22,26197,26198],{},"Environment#getProperty"," 读取",[320,26201,26202,26203,26205],{},"后续读取可以看到新 ",[22,26204,25283],{}," 中的值",[320,26207,26208],{},"适合少量需要实时读取的简单参数",[305,26210,26211,26215,26218],{},[320,26212,26213],{},[22,26214,25287],{},[320,26216,26217],{},"在 Spring Cloud 的变更事件处理机制下可以重新绑定",[320,26219,26220],{},"适合一组结构化、需要类型校验的配置",[305,26222,26223,26228,26234],{},[320,26224,26225,26226],{},"普通单例 Bean 中的 ",[22,26227,19830],{},[320,26229,26230,26231,26233],{},"不会仅因 ",[22,26232,20643],{}," 被替换而自动重新注入",[320,26235,26236,26237,26239],{},"将 Bean 放入 ",[22,26238,25294],{},"，或改用配置对象和显式监听",[305,26241,26242,26245,26248],{},[320,26243,26244],{},"初始化时根据配置创建的组件",[320,26246,26247],{},"已有组件不会自动重建",[320,26249,26250,26251,26253],{},"通过 ",[22,26252,25294],{}," 重建，或编写明确的组件更新逻辑",[305,26255,26256,26259,26262],{},[320,26257,26258],{},"不可变配置快照",[320,26260,26261],{},"构造新对象后整体替换引用",[320,26263,26264],{},"适合高并发读取和原子切换",[18,26266,26267,26269,26270,26272],{},[22,26268,19830],{}," 的注入发生在 Bean 创建和属性填充阶段。运行期间即使 ",[22,26271,20643],{}," 已经包含新值，普通单例 Bean 中原来注入的字段也不会因此自动再执行一次注入。",[6462,26274,26276],{"id":26275},"_43-configurationproperties-的绑定与刷新机制","4.3 @ConfigurationProperties 的绑定与刷新机制",[18,26278,26279,26281],{},[22,26280,25287],{}," 用来把具有相同前缀的一组外部属性绑定成一个类型明确的 Java 对象。例如：",[144,26283,26285],{"className":197,"code":26284,"language":199,"meta":150,"style":150},"@ConfigurationProperties(prefix = \"payment\")\npublic class PaymentProperties {\n\n    private Duration timeout;\n    private int retryCount;\n\n    public void setTimeout(Duration timeout) {\n        this.timeout = timeout;\n    }\n\n    public void setRetryCount(int retryCount) {\n        this.retryCount = retryCount;\n    }\n}\n",[22,26286,26287,26306,26317,26321,26327,26335,26339,26354,26364,26368,26372,26389,26399,26403],{"__ignoreMap":150},[203,26288,26289,26291,26294,26296,26299,26301,26304],{"class":205,"line":206},[203,26290,20011],{"class":219},[203,26292,26293],{"class":209},"ConfigurationProperties",[203,26295,220],{"class":219},[203,26297,26298],{"class":718},"prefix",[203,26300,9514],{"class":209},[203,26302,26303],{"class":223}," \"payment\"",[203,26305,12343],{"class":219},[203,26307,26308,26310,26312,26315],{"class":205,"line":251},[203,26309,441],{"class":209},[203,26311,1536],{"class":209},[203,26313,26314],{"class":215}," PaymentProperties",[203,26316,248],{"class":219},[203,26318,26319],{"class":205,"line":260},[203,26320,757],{"emptyLinePlaceholder":756},[203,26322,26323,26325],{"class":205,"line":278},[203,26324,17969],{"class":209},[203,26326,25900],{"class":219},[203,26328,26329,26331,26333],{"class":205,"line":285},[203,26330,17969],{"class":209},[203,26332,25909],{"class":209},[203,26334,25912],{"class":219},[203,26336,26337],{"class":205,"line":541},[203,26338,757],{"emptyLinePlaceholder":756},[203,26340,26341,26343,26345,26348,26350,26352],{"class":205,"line":549},[203,26342,1555],{"class":209},[203,26344,567],{"class":209},[203,26346,26347],{"class":215}," setTimeout",[203,26349,25924],{"class":219},[203,26351,25927],{"class":271},[203,26353,275],{"class":219},[203,26355,26356,26358,26360,26362],{"class":205,"line":787},[203,26357,25942],{"class":718},[203,26359,25945],{"class":219},[203,26361,495],{"class":209},[203,26363,25950],{"class":219},[203,26365,26366],{"class":205,"line":799},[203,26367,751],{"class":219},[203,26369,26370],{"class":205,"line":1371},[203,26371,757],{"emptyLinePlaceholder":756},[203,26373,26374,26376,26378,26381,26383,26385,26387],{"class":205,"line":17966},[203,26375,1555],{"class":209},[203,26377,567],{"class":209},[203,26379,26380],{"class":215}," setRetryCount",[203,26382,220],{"class":219},[203,26384,25932],{"class":209},[203,26386,25935],{"class":271},[203,26388,275],{"class":219},[203,26390,26391,26393,26395,26397],{"class":205,"line":17989},[203,26392,25942],{"class":718},[203,26394,25957],{"class":219},[203,26396,495],{"class":209},[203,26398,25912],{"class":219},[203,26400,26401],{"class":205,"line":17994},[203,26402,751],{"class":219},[203,26404,26405],{"class":205,"line":18015},[203,26406,288],{"class":219},[18,26408,26409],{},"应用启动时，其核心过程可以简化为：",[144,26411,26414],{"className":26412,"code":26413,"language":149,"meta":150},[147],"Environment 中的多个 PropertySource\n→ 按优先级解析 payment.* 属性\n→ ConfigurationPropertiesBindingPostProcessor 触发绑定\n→ Binder 完成名称匹配、类型转换和校验\n→ 将结果绑定到 PaymentProperties Bean\n",[22,26415,26413],{"__ignoreMap":150},[18,26417,26418,26421,26422,26424,26425,33],{},[22,26419,26420],{},"ConfigurationPropertiesBindingPostProcessor"," 是参与 Bean 初始化的后处理器，负责识别需要进行配置绑定的 Bean；真正读取 ",[22,26423,20643],{},"、匹配属性名称并进行类型转换的核心组件是 ",[22,26426,26427],{},"Binder",[18,26429,26430,26431,26433,26434,26436],{},"与单个 ",[22,26432,19830],{}," 相比，",[22,26435,25287],{}," 更适合结构化配置，因为它支持前缀聚合、宽松名称匹配、类型转换、嵌套对象和配置校验。",[18,26438,26439,26440,26442,26443,26445],{},"运行期间配置变化后，Spring Cloud 的刷新机制会根据最新 ",[22,26441,20643],{}," 再次执行绑定，使相应的 ",[22,26444,25287],{}," Bean 更新。对于基于 Setter 的可变配置对象，通常是在原 Bean 上重新设置属性，而不是一定创建一个新对象。因此需要注意：",[159,26447,26448,26451,26454,26460],{},[162,26449,26450],{},"多个字段重新绑定期间，不天然等于一次原子快照切换；",[162,26452,26453],{},"业务代码不应在 Setter 中执行网络请求等重副作用；",[162,26455,26456,26457,26459],{},"当前 Spring Cloud Commons 文档明确说明，使用 Java Record 声明的 ",[22,26458,25287],{}," 不能通过该机制动态刷新；构造器绑定或其他不可变形式也应结合实际 Spring Boot、Spring Cloud 版本验证；",[162,26461,26462],{},"要求多字段严格一致时，更适合先构造完整不可变对象，再原子替换引用。",[6462,26464,26466],{"id":26465},"_44-environmentchangeevent-负责通知哪些-key-变了","4.4 EnvironmentChangeEvent 负责通知“哪些 Key 变了”",[18,26468,26469,26470,26473],{},"Spring Cloud 监听 ",[22,26471,26472],{},"EnvironmentChangeEvent","。事件中包含发生变化的 Key，例如：",[144,26475,26478],{"className":26476,"code":26477,"language":149,"meta":150},[147],"payment.timeout\npayment.retry-count\nlogging.level.com.example.payment\n",[22,26479,26477],{"__ignoreMap":150},[18,26481,26482],{},"收到事件后，框架可以：",[159,26484,26485,26491,26498],{},[162,26486,26487,26488,26490],{},"重新绑定受影响的 ",[22,26489,25287],{}," Bean；",[162,26492,26493,26494,26497],{},"更新 ",[22,26495,26496],{},"logging.level.*"," 对应的日志级别；",[162,26499,26500],{},"触发应用自行注册的变更监听器。",[18,26502,26503],{},"因此顺序应该是：",[144,26505,26508],{"className":26506,"code":26507,"language":149,"meta":150},[147],"替换远程 PropertySource\n→ 发布 EnvironmentChangeEvent(changedKeys)\n→ 重新绑定或执行监听逻辑\n",[22,26509,26507],{"__ignoreMap":150},[18,26511,26512,26513,26515],{},"事件本身通常不携带完整配置内容或配置对象，也不应该绕过 ",[22,26514,20643],{}," 直接修改业务字段。它表达的是“这些属性发生了变化”，真正的新值仍从已经更新的属性源中读取。",[6462,26517,26519],{"id":26518},"_45-refreshscope-如何让-bean-使用新值","4.5 @RefreshScope 如何让 Bean 使用新值",[18,26521,26522],{},"有些 Bean 在创建时读取配置并形成内部状态，仅重新绑定属性对象并不够。例如：",[144,26524,26526],{"className":197,"code":26525,"language":199,"meta":150,"style":150},"@RefreshScope\n@Service\npublic class PaymentClient {\n\n    private final Duration timeout;\n\n    public PaymentClient(\n            @Value(\"${payment.timeout}\") Duration timeout) {\n        this.timeout = timeout;\n    }\n}\n",[22,26527,26528,26535,26542,26552,26556,26564,26568,26576,26596,26606,26610],{"__ignoreMap":150},[203,26529,26530,26532],{"class":205,"line":206},[203,26531,20011],{"class":219},[203,26533,26534],{"class":209},"RefreshScope\n",[203,26536,26537,26539],{"class":205,"line":251},[203,26538,20011],{"class":219},[203,26540,26541],{"class":209},"Service\n",[203,26543,26544,26546,26548,26550],{"class":205,"line":260},[203,26545,441],{"class":209},[203,26547,1536],{"class":209},[203,26549,20785],{"class":215},[203,26551,248],{"class":219},[203,26553,26554],{"class":205,"line":278},[203,26555,757],{"emptyLinePlaceholder":756},[203,26557,26558,26560,26562],{"class":205,"line":285},[203,26559,17969],{"class":209},[203,26561,17952],{"class":209},[203,26563,25900],{"class":219},[203,26565,26566],{"class":205,"line":541},[203,26567,757],{"emptyLinePlaceholder":756},[203,26569,26570,26572,26574],{"class":205,"line":549},[203,26571,1555],{"class":209},[203,26573,20785],{"class":215},[203,26575,1496],{"class":219},[203,26577,26578,26581,26584,26586,26589,26592,26594],{"class":205,"line":787},[203,26579,26580],{"class":219},"            @",[203,26582,26583],{"class":209},"Value",[203,26585,220],{"class":219},[203,26587,26588],{"class":223},"\"${payment.timeout}\"",[203,26590,26591],{"class":219},") Duration ",[203,26593,25927],{"class":271},[203,26595,275],{"class":219},[203,26597,26598,26600,26602,26604],{"class":205,"line":799},[203,26599,25942],{"class":718},[203,26601,25945],{"class":219},[203,26603,495],{"class":209},[203,26605,25950],{"class":219},[203,26607,26608],{"class":205,"line":1371},[203,26609,751],{"class":219},[203,26611,26612],{"class":205,"line":17966},[203,26613,288],{"class":219},[18,26615,26616,26618,26619,26622],{},[22,26617,25294],{}," 是 Spring Cloud 提供的一种自定义 Bean Scope。容器注册 Bean 时，对外暴露的通常是一个作用域代理；真正的目标对象由 ",[22,26620,26621],{},"RefreshScope"," 延迟创建并缓存。其他 Bean 注入的是这个稳定的代理，而不是直接长期持有目标对象。",[18,26624,26625],{},"发生刷新时，核心过程是：",[144,26627,26630],{"className":26628,"code":26629,"language":149,"meta":150},[147],"更新 Environment\n→ 清除 RefreshScope 中指定 Bean 的目标对象缓存\n→ 销毁旧目标对象\n→ 代理本身仍保留在其他 Bean 中\n→ 下一次调用代理时重新执行 Bean 创建流程\n→ 从最新 Environment 注入 @Value 或其他依赖\n→ 缓存并使用新目标对象\n",[22,26631,26629],{"__ignoreMap":150},[18,26633,14473,26634,26636],{},[22,26635,25294],{}," 并不是在原对象上逐个修改字段，而是让代理在刷新后切换到重新创建的目标对象。",[18,26638,26639],{},"可以把过程理解为：",[144,26641,26644],{"className":26642,"code":26643,"language":149,"meta":150},[147],"业务 Bean 持有 PaymentClient 代理\n            ↓\n刷新前调用 → revision 101 对应的旧目标对象\n\n清除 RefreshScope 中的目标对象缓存\n            ↓\n刷新后第一次调用 → 使用 revision 102 创建新目标对象\n            ↓\n后续调用 → 使用新的目标对象\n",[22,26645,26643],{"__ignoreMap":150},[18,26647,26648,26649,26651],{},"刷新范围应尽量小，只重建确实需要更新的 Bean。刷新整个 ",[22,26650,20673],{}," 成本高、影响范围大，也更容易产生短暂不可用。",[18,26653,26654,26655,26657,26658,26660,26661,26663],{},"还要注意，给一个 ",[22,26656,19585],{}," 类添加 ",[22,26659,25294],{},"，并不意味着它声明的所有 ",[22,26662,19421],{}," 都自动进入刷新作用域；需要刷新哪些 Bean，应逐个确认依赖关系。",[18,26665,26666,40,26668,26670],{},[22,26667,25287],{},[22,26669,25294],{}," 解决的问题不同：",[299,26672,26673,26683],{},[302,26674,26675],{},[305,26676,26677,26680],{},[308,26678,26679],{},"机制",[308,26681,26682],{},"主要作用",[315,26684,26685,26697],{},[305,26686,26687,26691],{},[320,26688,26689],{},[22,26690,25287],{},[320,26692,26693,26694,26696],{},"把 ",[22,26695,20643],{}," 中的一组属性绑定为类型明确的配置对象，刷新时可以重新绑定",[305,26698,26699,26703],{},[320,26700,26701],{},[22,26702,25294],{},[320,26704,26705],{},"清除旧目标对象并在下次调用时重建 Bean，使只在创建阶段读取配置的组件重新初始化",[18,26707,26708,26709,26711,26712,26714,26715,26717],{},"一个 ",[22,26710,25294],{}," Bean 可以依赖 ",[22,26713,25287],{},"，也可以在构造器中使用 ",[22,26716,19830],{},"。是否组合使用，要看业务需要的是“更新配置字段”，还是“根据新配置重建整个组件”。",[6462,26719,26721],{"id":26720},"_46-结构化业务配置更适合不可变快照","4.6 结构化业务配置更适合不可变快照",[18,26723,26724],{},"对于路由规则、限流规则或多字段业务策略，仅依靠多个字段分别重新绑定，业务线程可能在切换过程中观察到不一致组合。更稳妥的方式是：",[144,26726,26728],{"className":197,"code":26727,"language":199,"meta":150,"style":150},"public record PaymentRule(\n        Duration timeout,\n        int retryCount,\n        boolean enabled) {\n}\n",[22,26729,26730,26742,26747,26754,26762],{"__ignoreMap":150},[203,26731,26732,26734,26737,26740],{"class":205,"line":206},[203,26733,441],{"class":209},[203,26735,26736],{"class":209}," record",[203,26738,26739],{"class":215}," PaymentRule",[203,26741,1496],{"class":219},[203,26743,26744],{"class":205,"line":251},[203,26745,26746],{"class":219},"        Duration timeout,\n",[203,26748,26749,26751],{"class":205,"line":260},[203,26750,17274],{"class":209},[203,26752,26753],{"class":219}," retryCount,\n",[203,26755,26756,26759],{"class":205,"line":278},[203,26757,26758],{"class":209},"        boolean",[203,26760,26761],{"class":219}," enabled) {\n",[203,26763,26764],{"class":205,"line":285},[203,26765,288],{"class":219},[18,26767,26768],{},"监听器收到新版本后：",[144,26770,26773],{"className":26771,"code":26772,"language":149,"meta":150},[147],"读取完整配置文档\n→ 类型转换和业务校验\n→ 构造新的 PaymentRule\n→ 一次性替换 volatile 或 AtomicReference 中的引用\n",[22,26774,26772],{"__ignoreMap":150},[18,26776,26777],{},"这样业务请求要么使用完整旧规则，要么使用完整新规则，不会同时看到新超时和旧重试次数。若校验失败，应保留旧对象并告警。",[6462,26779,26781],{"id":26780},"_47-线程池和连接池不能只更新一个数字","4.7 线程池和连接池不能只更新一个数字",[18,26783,26784],{},"有些配置代表正在运行的资源：",[159,26786,26787,26790,26793,26796,26799],{},[162,26788,26789],{},"线程池核心线程数和最大线程数；",[162,26791,26792],{},"数据库连接池地址和容量；",[162,26794,26795],{},"HTTP 客户端连接数、超时和证书；",[162,26797,26798],{},"MQ 消费并发度；",[162,26800,26801],{},"定时任务周期。",[18,26803,26804],{},"这类组件是否支持动态更新，取决于组件本身：",[144,26806,26809],{"className":26807,"code":26808,"language":149,"meta":150},[147],"支持安全 Setter 更新\n→ 按正确顺序修改参数并验证结果\n\n不支持原地更新但允许重建\n→ 创建新组件 → 健康检查 → 原子切换 → 优雅关闭旧组件\n\n不支持安全热更新\n→ 标记为“重启生效”，不要伪装成动态配置\n",[22,26810,26808],{"__ignoreMap":150},[18,26812,26813,26814,33],{},"例如同时修改线程池的核心线程数和最大线程数时，还要根据增大或减小选择设置顺序，否则中间状态可能违反 ",[22,26815,26816],{},"corePoolSize \u003C= maximumPoolSize",[6462,26818,26820],{"id":26819},"_48-一次可靠的-spring-刷新流程","4.8 一次可靠的 Spring 刷新流程",[18,26822,26823],{},"可以将一次配置刷新设计为：",[144,26825,26828],{"className":26826,"code":26827,"language":149,"meta":150},[147],"1. SDK 收到 configId + revision 通知\n2. 拉取完整新内容\n3. 校验 revision、摘要、格式和业务规则\n4. 保存本地最后正确快照\n5. 替换 Spring Environment 中的远程 PropertySource\n6. 计算 changedKeys\n7. 发布 EnvironmentChangeEvent\n8. 重新绑定 @ConfigurationProperties\n9. 清除指定 RefreshScope Bean 的目标缓存\n10. 执行需要显式切换资源的业务监听器\n11. 上报已应用 revision、耗时和结果\n",[22,26829,26827],{"__ignoreMap":150},[18,26831,26832],{},"如果第 8～10 步失败，不能只记录“配置拉取成功”。客户端应分别记录：",[144,26834,26837],{"className":26835,"code":26836,"language":149,"meta":150},[147],"receivedRevision：已经收到的版本\nappliedRevision：业务已经成功应用的版本\n",[22,26838,26836],{"__ignoreMap":150},[18,26840,191,26841,26844],{},[22,26842,26843],{},"appliedRevision"," 更新后，平台才能认为配置真正生效。否则控制台显示“所有客户端已收到”，实际业务仍可能运行在旧配置上。",[14,26846,26848],{"id":26847},"八灰度发布如何设计","八、灰度发布如何设计",[18,26850,26851],{},"高风险配置不应直接全量生效。灰度规则可以按以下维度匹配：",[159,26853,26854,26857,26860,26863,26866],{},[162,26855,26856],{},"实例 IP；",[162,26858,26859],{},"实例标签；",[162,26861,26862],{},"集群或机房；",[162,26864,26865],{},"用户或租户范围；",[162,26867,26868],{},"固定实例百分比。",[18,26870,26871],{},"推荐流程：",[144,26873,26876],{"className":26874,"code":26875,"language":149,"meta":150},[147],"创建灰度版本\n→ 命中少量实例\n→ 观察错误率、延迟和业务指标\n→ 扩大灰度范围\n→ 转为正式版本\n\n异常时\n→ 停止灰度\n→ 客户端恢复正式版本\n",[22,26877,26875],{"__ignoreMap":150},[18,26879,26880],{},"灰度内容是同一个配置资源的另一个发布状态，不应伪装成新的配置 Key。客户端查询时，服务端根据客户端标签决定返回灰度版本还是正式版本，并在响应中明确标记版本类型。",[18,26882,26883],{},"对于功能开关，还可以同时在业务代码中保留熔断或总开关，防止配置中心的灰度规则与业务规则互相覆盖而难以理解。",[14,26885,26887],{"id":26886},"九存储与服务端架构","九、存储与服务端架构",[473,26889,26891],{"id":26890},"_1-权威存储","1. 权威存储",[18,26893,26894],{},"需要保存：",[159,26896,26897,26900,26903,26906,26909,26911,26914,26917],{},[162,26898,26899],{},"配置身份和元数据；",[162,26901,26902],{},"当前正式版本指针；",[162,26904,26905],{},"不可变发布版本；",[162,26907,26908],{},"草稿；",[162,26910,25581],{},[162,26912,26913],{},"发布历史；",[162,26915,26916],{},"权限和审批记录；",[162,26918,26919],{},"Outbox 变更事件。",[18,26921,26922],{},"普通企业配置中心可以使用高可用关系型数据库作为权威存储，利用事务、唯一索引和审计查询能力。需要严格线性一致的通用协调能力时，也可以采用基于 Raft 的一致性 KV 存储，但这会增加部署和运维复杂度。",[18,26924,26925],{},"服务端本地缓存、Redis 或本地 Dump 都只能用于加速和容灾，不能成为绕过权威存储的另一个真相来源。",[473,26927,26929],{"id":26928},"_2-服务节点","2. 服务节点",[18,26931,26932],{},"运行时查询和通知节点尽量设计为无状态：",[144,26934,26937],{"className":26935,"code":26936,"language":149,"meta":150},[147],"Client SDK\n    ↓\n负载均衡 \u002F 服务发现\n    ↓\nConfig Server × N\n    ↓\n权威存储\n",[22,26938,26936],{"__ignoreMap":150},[18,26940,26941],{},"节点在本地维护热点配置缓存和订阅关系。节点故障后，客户端能够重连到其他节点并根据本地版本继续监听。",[473,26943,26945],{"id":26944},"_3-变更传播","3. 变更传播",[18,26947,26948],{},"发布节点写入权威存储后，需要让其他服务节点刷新缓存。可以使用：",[159,26950,26951,26954,26957,26960],{},[162,26952,26953],{},"数据库 Outbox + MQ；",[162,26955,26956],{},"数据库变更日志 CDC；",[162,26958,26959],{},"一致性存储的 Watch；",[162,26961,26962],{},"定时扫描版本表。",[18,26964,26965],{},"CDC（Change Data Capture，变更数据捕获）是从数据库 Binlog 等变更日志中读取已提交的数据变化，再转换成事件通知其他节点。它能减少业务代码中的消息双写，但仍需要处理重复事件、延迟和断点恢复。",[18,26967,26968],{},"无论选择哪种方式，都要有版本校准，防止某个服务节点因漏事件长期返回旧配置。",[14,26970,26972],{"id":26971},"十容量与性能怎么估算","十、容量与性能怎么估算",[18,26974,26975],{},"配置中心的主要压力不是配置写入，而是：",[159,26977,26978,26981,26984,26987,26990,26993],{},[162,26979,26980],{},"客户端连接数；",[162,26982,26983],{},"启动时的全量拉取；",[162,26985,26986],{},"发布瞬间的通知扇出；",[162,26988,26989],{},"大量客户端同时回拉内容；",[162,26991,26992],{},"热点公共配置；",[162,26994,26995],{},"本地缓存和订阅关系占用。",[18,26997,26998],{},"需要重点估算：",[144,27000,27003],{"className":27001,"code":27002,"language":149,"meta":150},[147],"实例总数\n× 每个实例监听的配置数量\n= 总订阅关系\n",[22,27004,27002],{"__ignoreMap":150},[18,27006,27007],{},"以及：",[144,27009,27012],{"className":27010,"code":27011,"language":149,"meta":150},[147],"单次配置大小\n× 命中实例数\n÷ 期望分发时间\n= 发布后的网络吞吐需求\n",[22,27013,27011],{"__ignoreMap":150},[18,27015,27016],{},"常见优化包括：",[159,27018,27019,27022,27025,27028,27031,27034,27037],{},[162,27020,27021],{},"通知只携带配置标识和版本；",[162,27023,27024],{},"相同配置的通知批量合并；",[162,27026,27027],{},"客户端回拉增加随机抖动，避免惊群；",[162,27029,27030],{},"服务端缓存热点正式版本；",[162,27032,27033],{},"对配置大小、数量和发布频率设置配额；",[162,27035,27036],{},"公共配置分层缓存；",[162,27038,27039],{},"大规模发布时分批推进。",[18,27041,27042],{},"不能为了实时性让所有客户端在同一毫秒回源，应通过随机抖动、缓存和分批推进控制集中回拉造成的瞬时压力。",[14,27044,27046],{"id":27045},"十一权限安全和审计","十一、权限、安全和审计",[18,27048,27049],{},"配置中心通常可以直接改变生产行为，因此权限应比普通管理后台更严格。",[18,27051,27052],{},"至少需要：",[159,27054,27055,27058,27061,27064,27067,27070,27073,27076,27079],{},[162,27056,27057],{},"SSO 或统一身份认证；",[162,27059,27060],{},"基于租户、环境、应用和命名空间的 RBAC；",[162,27062,27063],{},"编辑、审批、发布、回滚权限分离；",[162,27065,27066],{},"生产环境高风险配置双人复核；",[162,27068,27069],{},"API Token 最小权限和定期轮换；",[162,27071,27072],{},"传输层 TLS；",[162,27074,27075],{},"敏感字段加密或脱敏展示；",[162,27077,27078],{},"完整的操作人、时间、来源、Diff 和审批记录；",[162,27080,27081],{},"批量导出、删除和回滚的风险控制。",[18,27083,27084],{},"其中，SSO（Single Sign-On）指统一登录；RBAC（Role-Based Access Control）指基于角色分配权限，例如开发者可以编辑测试环境配置，但只有发布者或审批者能够变更生产环境。",[18,27086,27087],{},"应用 SDK 通常只需要读取自己所属范围的配置，不应拥有发布权限。配置服务也不应直接暴露在公网。",[14,27089,27091],{"id":27090},"十二可观测性","十二、可观测性",[18,27093,27094],{},"平台侧建议监控：",[159,27096,27097,27100,27103,27106,27109,27112,27115,27118,27121,27124],{},[162,27098,27099],{},"发布成功率和发布延迟；",[162,27101,27102],{},"通知延迟和积压；",[162,27104,27105],{},"在线连接数与重连次数；",[162,27107,27108],{},"各版本客户端数量；",[162,27110,27111],{},"配置查询 QPS、P95\u002FP99 延迟和错误率；",[162,27113,27114],{},"服务端缓存命中率；",[162,27116,27117],{},"版本校准发现的不一致数量；",[162,27119,27120],{},"本地快照降级启动数量；",[162,27122,27123],{},"慢监听器和回调失败次数；",[162,27125,27126],{},"灰度实例范围与实际命中数量。",[18,27128,27129],{},"非常重要的一项是客户端版本分布：",[144,27131,27134],{"className":27132,"code":27133,"language":149,"meta":150},[147],"revision 103：9,980 个实例\nrevision 102：20 个实例\n",[22,27135,27133],{"__ignoreMap":150},[18,27137,27138],{},"它能回答“配置是否真的生效”，而不是只证明“控制台发布成功”。对长期停留在旧版本的实例，应能查询其连接节点、最近心跳、拉取错误和本地版本。",[14,27140,27142],{"id":27141},"十三故障场景要提前设计","十三、故障场景要提前设计",[299,27144,27145,27155],{},[302,27146,27147],{},[305,27148,27149,27152],{},[308,27150,27151],{},"故障",[308,27153,27154],{},"系统应该如何处理",[315,27156,27157,27165,27173,27181,27189,27197,27205,27213,27221],{},[305,27158,27159,27162],{},[320,27160,27161],{},"数据库短暂不可用",[320,27163,27164],{},"禁止新发布；读取节点使用已确认缓存，恢复后校准",[305,27166,27167,27170],{},[320,27168,27169],{},"某个配置服务节点故障",[320,27171,27172],{},"客户端重连其他节点，携带当前版本继续监听",[305,27174,27175,27178],{},[320,27176,27177],{},"通知消息丢失",[320,27179,27180],{},"客户端定时校准发现新版本并补拉",[305,27182,27183,27186],{},[320,27184,27185],{},"客户端收到乱序通知",[320,27187,27188],{},"比较 revision，只允许版本前进",[305,27190,27191,27194],{},[320,27192,27193],{},"新内容格式错误",[320,27195,27196],{},"发布前校验；客户端再次校验，失败后保留旧版本",[305,27198,27199,27202],{},[320,27200,27201],{},"业务监听器执行失败",[320,27203,27204],{},"隔离回调、告警并保留旧的业务快照或按策略重试",[305,27206,27207,27210],{},[320,27208,27209],{},"错误配置已经全量发布",[320,27211,27212],{},"以历史内容生成新版本并紧急发布",[305,27214,27215,27218],{},[320,27216,27217],{},"大量实例同时启动",[320,27219,27220],{},"启动随机抖动、限流、缓存和分批扩容",[305,27222,27223,27226],{},[320,27224,27225],{},"本地快照损坏",[320,27227,27228],{},"摘要校验失败后拒绝使用，回源或失败退出",[18,27230,27231],{},"配置中心追求的不是“永远不会失败”，而是每个失败点都有确定的降级行为，并且不会悄悄把错误配置传播给更多实例。",[14,27233,27235],{"id":27234},"十四常见设计误区","十四、常见设计误区",[473,27237,27239],{"id":27238},"_1-只做一张配置表和几个-crud-接口","1. 只做一张配置表和几个 CRUD 接口",[18,27241,27242],{},"这只能算配置管理页面，缺少版本、发布、通知、客户端容灾、灰度、权限和审计，无法承担生产配置中心职责。",[473,27244,27246],{"id":27245},"_2-每次编辑立即推送线上","2. 每次编辑立即推送线上",[18,27248,27249],{},"编辑与发布不分离，会放大误操作风险，也无法进行审批、Diff 和定时发布。",[473,27251,27253],{"id":27252},"_3-把-mq-通知当成唯一正确性保障","3. 把 MQ 通知当成唯一正确性保障",[18,27255,27256],{},"MQ 只能提高变更传播效率，不能代替权威版本查询和客户端定时校准。",[473,27258,27260],{"id":27259},"_4-服务端把完整配置直接推给所有客户端","4. 服务端把完整配置直接推给所有客户端",[18,27262,27263],{},"大配置和大规模实例会造成瞬时流量放大，而且推送路径还要重复实现查询链路中的鉴权、灰度和校验逻辑。通常只推变更标识和版本更稳妥。",[473,27265,27267],{"id":27266},"_5-客户端原地修改可变对象","5. 客户端原地修改可变对象",[18,27269,27270],{},"多个字段逐个更新时，业务线程可能读取到中间状态。应该构造完整新快照后原子替换。",[473,27272,27274],{"id":27273},"_6-所有参数都支持热更新","6. 所有参数都支持热更新",[18,27276,27277],{},"数据库连接池、线程池、序列化协议等配置是否能安全热更新，取决于组件能力。每个配置应标记：",[144,27279,27282],{"className":27280,"code":27281,"language":149,"meta":150},[147],"动态立即生效\n动态但需要组件重建\n只在应用启动时读取\n变更后必须重新部署\n",[22,27283,27281],{"__ignoreMap":150},[18,27285,27286],{},"配置中心负责传递变化，不会自动让不支持热更新的代码变得安全。",[14,27288,27290],{"id":27289},"十五一个可落地的最小版本","十五、一个可落地的最小版本",[18,27292,27293],{},"如果从零建设，可以分阶段实现。",[473,27295,27297],{"id":27296},"第一阶段正确发布","第一阶段：正确发布",[159,27299,27300,27303,27306,27309,27312,27315],{},[162,27301,27302],{},"环境、应用和命名空间隔离；",[162,27304,27305],{},"草稿与正式发布分离；",[162,27307,27308],{},"不可变版本和历史；",[162,27310,27311],{},"乐观锁；",[162,27313,27314],{},"权限和审计；",[162,27316,27317],{},"客户端查询与本地快照。",[473,27319,27321],{"id":27320},"第二阶段动态分发","第二阶段：动态分发",[159,27323,27324,27327,27330,27333,27336,27339],{},[162,27325,27326],{},"长轮询或长连接；",[162,27328,27329],{},"版本通知；",[162,27331,27332],{},"客户端监听器；",[162,27334,27335],{},"定时版本校准；",[162,27337,27338],{},"服务端缓存和多实例部署；",[162,27340,27341],{},"客户端版本上报。",[473,27343,27345],{"id":27344},"第三阶段发布安全与规模化","第三阶段：发布安全与规模化",[159,27347,27348,27351,27354,27357,27360,27363,27366],{},[162,27349,27350],{},"灰度发布；",[162,27352,27353],{},"审批流；",[162,27355,27356],{},"规则校验；",[162,27358,27359],{},"一键回滚；",[162,27361,27362],{},"容量配额；",[162,27364,27365],{},"多机房容灾；",[162,27367,27368],{},"发布效果观测和全链路告警。",[18,27370,27371],{},"优先保证版本正确、能够回滚和客户端可恢复，再优化毫秒级通知。对配置中心而言，错误配置传播得越快，故障也会扩散得越快。",[14,27373,5803],{"id":5803},[159,27375,27376,27383,27390,27397,27404,27411,27418,27425,27432],{},[162,27377,27378],{},[119,27379,27382],{"href":27380,"rel":27381},"https:\u002F\u002Fnacos.io\u002Fen\u002Fdocs\u002Fnext\u002Fmanual\u002Fuser\u002Fconfig\u002Foverview\u002F",[2080],"Nacos：Configuration Overview",[162,27384,27385],{},[119,27386,27389],{"href":27387,"rel":27388},"https:\u002F\u002Fnacos.io\u002Fen\u002Fdocs\u002Flatest\u002Fmanual\u002Fuser\u002Fconfig\u002Fpublish-query-listen\u002F",[2080],"Nacos：Publish, Query, And Listen",[162,27391,27392],{},[119,27393,27396],{"href":27394,"rel":27395},"https:\u002F\u002Fwww.nacos.io\u002Fen\u002Fdocs\u002Fnext\u002Fmanual\u002Fuser\u002Fconfig\u002Fgray-release\u002F",[2080],"Nacos：Configuration Gray Release",[162,27398,27399],{},[119,27400,27403],{"href":27401,"rel":27402},"https:\u002F\u002Fnacos.io\u002Fen\u002Fdocs\u002Flatest\u002Fmanual\u002Fuser\u002Fconfig\u002Fops-and-troubleshooting\u002F",[2080],"Nacos：Operations And Troubleshooting",[162,27405,27406],{},[119,27407,27410],{"href":27408,"rel":27409},"https:\u002F\u002Fgithub.com\u002Fapolloconfig\u002Fapollo\u002Fwiki\u002FApollo%E9%85%8D%E7%BD%AE%E4%B8%AD%E5%BF%83%E8%AE%BE%E8%AE%A1",[2080],"Apollo：配置中心设计",[162,27412,27413],{},[119,27414,27417],{"href":27415,"rel":27416},"https:\u002F\u002Fetcd.io\u002Fdocs\u002Fv3.5\u002Flearning\u002Fapi_guarantees\u002F",[2080],"etcd：API Guarantees",[162,27419,27420],{},[119,27421,27424],{"href":27422,"rel":27423},"https:\u002F\u002Fdeveloper.hashicorp.com\u002Fconsul\u002Fdocs\u002Fautomate\u002Fwatch",[2080],"Consul：Watches Overview",[162,27426,27427],{},[119,27428,27431],{"href":27429,"rel":27430},"https:\u002F\u002Fdocs.spring.io\u002Fspring-boot\u002Freference\u002Ffeatures\u002Fexternal-config.html",[2080],"Spring Boot：Externalized Configuration",[162,27433,27434],{},[119,27435,27438],{"href":27436,"rel":27437},"https:\u002F\u002Fdocs.spring.io\u002Fspring-cloud-commons\u002Freference\u002Fspring-cloud-commons\u002Fapplication-context-services.html",[2080],"Spring Cloud Commons：Environment Changes 与 Refresh Scope",[2111,27440,27441],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":150,"searchDepth":251,"depth":251,"links":27443},[27444,27445,27446,27447,27448,27452,27456,27462,27468,27474,27480,27481,27486,27487,27488,27489,27490,27498,27503],{"id":16,"depth":251,"text":16},{"id":115,"depth":251,"text":115},{"id":132,"depth":251,"text":132},{"id":25321,"depth":251,"text":25322},{"id":25398,"depth":251,"text":25399,"children":27449},[27450,27451],{"id":25482,"depth":260,"text":25483},{"id":25510,"depth":260,"text":25511},{"id":25556,"depth":251,"text":25557,"children":27453},[27454,27455],{"id":25560,"depth":260,"text":25561},{"id":25593,"depth":260,"text":25594},{"id":25623,"depth":251,"text":25624,"children":27457},[27458,27459,27460,27461],{"id":25636,"depth":260,"text":25637},{"id":25663,"depth":260,"text":25664},{"id":25690,"depth":260,"text":25691},{"id":25706,"depth":260,"text":25707},{"id":25725,"depth":251,"text":25726,"children":27463},[27464,27465,27466,27467],{"id":25732,"depth":260,"text":25733},{"id":25748,"depth":260,"text":25749},{"id":25758,"depth":260,"text":25759},{"id":25782,"depth":260,"text":25783},{"id":25798,"depth":251,"text":25799,"children":27469},[27470,27471,27472,27473],{"id":25822,"depth":260,"text":25823},{"id":25829,"depth":260,"text":25830},{"id":25842,"depth":260,"text":25843},{"id":25870,"depth":260,"text":25871},{"id":26017,"depth":251,"text":26018,"children":27475},[27476,27477,27478,27479],{"id":26030,"depth":260,"text":26031},{"id":26046,"depth":260,"text":26047},{"id":26096,"depth":260,"text":26097},{"id":26123,"depth":260,"text":26124},{"id":26847,"depth":251,"text":26848},{"id":26886,"depth":251,"text":26887,"children":27482},[27483,27484,27485],{"id":26890,"depth":260,"text":26891},{"id":26928,"depth":260,"text":26929},{"id":26944,"depth":260,"text":26945},{"id":26971,"depth":251,"text":26972},{"id":27045,"depth":251,"text":27046},{"id":27090,"depth":251,"text":27091},{"id":27141,"depth":251,"text":27142},{"id":27234,"depth":251,"text":27235,"children":27491},[27492,27493,27494,27495,27496,27497],{"id":27238,"depth":260,"text":27239},{"id":27245,"depth":260,"text":27246},{"id":27252,"depth":260,"text":27253},{"id":27259,"depth":260,"text":27260},{"id":27266,"depth":260,"text":27267},{"id":27273,"depth":260,"text":27274},{"id":27289,"depth":251,"text":27290,"children":27499},[27500,27501,27502],{"id":27296,"depth":260,"text":27297},{"id":27320,"depth":260,"text":27321},{"id":27344,"depth":260,"text":27345},{"id":5803,"depth":251,"text":5803},"wiki:java:how-to-design-a-configuration-center","从配置模型、管理面与分发面、发布流程、推拉结合、版本一致性、本地容灾、Spring 动态刷新、灰度回滚、权限审计和容量治理，系统讲解配置中心的设计方法。",{},83,"\u002Fjava\u002Fhow-to-design-a-configuration-center",{"title":25258,"description":27505},"java\u002Fhow-to-design-a-configuration-center","2026-07-31","qZy75UH-4auSNXHGeAua8V-8unzvIuA989ZLU2drY_Y",{"id":27514,"title":27515,"body":27516,"commentId":29188,"description":29189,"difficulty":2147,"draft":2148,"extension":2149,"meta":29190,"navigation":756,"order":29191,"path":29192,"section":2153,"seo":29193,"stem":29194,"updated":29195,"__hash__":29196},"java\u002Fjava\u002Fhow-to-implement-singleton-pattern.md","Java 如何实现单例模式？",{"type":7,"value":27517,"toc":29161},[27518,27521,27523,27526,27533,27536,27538,27545,27547,27556,27558,27562,27565,27573,27576,27582,27585,27589,27592,27682,27689,27718,27724,27730,27740,27750,27753,27764,27767,27778,27781,27785,27788,27892,27903,27909,27933,27939,27948,27954,27965,27968,27972,27975,28116,28120,28123,28142,28145,28148,28193,28196,28200,28203,28209,28212,28227,28232,28240,28244,28247,28450,28453,28501,28507,28510,28524,28527,28531,28534,28641,28647,28650,28654,28658,28714,28721,28725,28791,28794,28798,28802,28805,28861,28864,28868,28875,28901,28904,28908,28922,28926,28929,28932,28937,28940,28944,29045,29049,29052,29116,29126,29132,29136,29158],[10,27519,27515],{"id":27520},"java-如何实现单例模式",[14,27522,16],{"id":16},[18,27524,27525],{},"单例模式用于保证一个类在指定作用域内只有一个实例，并提供统一的访问入口。Java 中常见实现包括饿汉式、静态内部类、双重检查锁和枚举。",[18,27527,27528,27529,27532],{},"饿汉式依靠 JVM 的类初始化机制保证线程安全，实现简单，但不能按需加载；静态内部类同时具备线程安全和懒加载，通常是普通业务代码中的推荐实现；双重检查锁适合需要显式控制懒加载过程的场景，但实例引用必须使用 ",[22,27530,27531],{},"volatile","，防止对象发布与初始化发生不安全的重排序；枚举写法最简洁，还能天然抵御常规反射和反序列化破坏，是实现严格单例的可靠方式。",[18,27534,27535],{},"需要注意，单例的唯一性通常只限定在同一个 ClassLoader 中。分布式系统中的“全局唯一实例”不能只靠 Java 单例模式实现。",[18,27537,103],{},[105,27539,27540],{},[18,27541,27542],{},[110,27543,27544],{},"普通懒加载优先使用静态内部类，强调防反射和反序列化时使用枚举；使用双重检查锁时，实例引用必须声明为 volatile。",[14,27546,115],{"id":115},[18,27548,27549],{},[119,27550,27553],{"href":27551,"target":122,"rel":27552},"\u002Fimages\u002Fwiki\u002Fjava\u002Fhow-to-implement-singleton-pattern.svg",[124,125],[127,27554],{"src":27551,"alt":27555},"Java 单例模式四种常见实现及选择方式",[14,27557,132],{"id":132},[14,27559,27561],{"id":27560},"一什么是单例模式","一、什么是单例模式",[18,27563,27564],{},"单例模式主要解决两个问题：",[2695,27566,27567,27570],{},[162,27568,27569],{},"限制对象创建，保证一个类在指定作用域内只有一个实例；",[162,27571,27572],{},"提供统一入口，让其他代码能够获取这个实例。",[18,27574,27575],{},"一个典型的单例类通常包含：",[144,27577,27580],{"className":27578,"code":27579,"language":149,"meta":150},[147],"私有构造方法\n+ 类内部持有唯一实例\n+ 对外提供获取实例的方法\n",[22,27581,27579],{"__ignoreMap":150},[18,27583,27584],{},"例如配置管理器、无状态工具服务或共享资源协调器，可能只需要一个实例。单例并不等于全局变量，它还负责控制对象的创建过程。",[14,27586,27588],{"id":27587},"二饿汉式","二、饿汉式",[18,27590,27591],{},"饿汉式在类初始化阶段直接创建对象：",[144,27593,27595],{"className":197,"code":27594,"language":199,"meta":150,"style":150},"public final class Singleton {\n\n    private static final Singleton INSTANCE = new Singleton();\n\n    private Singleton() {\n    }\n\n    public static Singleton getInstance() {\n        return INSTANCE;\n    }\n}\n",[22,27596,27597,27610,27614,27633,27637,27645,27649,27653,27667,27674,27678],{"__ignoreMap":150},[203,27598,27599,27601,27603,27605,27608],{"class":205,"line":206},[203,27600,441],{"class":209},[203,27602,17952],{"class":209},[203,27604,1536],{"class":209},[203,27606,27607],{"class":215}," Singleton",[203,27609,248],{"class":219},[203,27611,27612],{"class":205,"line":251},[203,27613,757],{"emptyLinePlaceholder":756},[203,27615,27616,27618,27620,27622,27625,27627,27629,27631],{"class":205,"line":260},[203,27617,17969],{"class":209},[203,27619,17972],{"class":209},[203,27621,17952],{"class":209},[203,27623,27624],{"class":219}," Singleton INSTANCE ",[203,27626,495],{"class":209},[203,27628,212],{"class":209},[203,27630,27607],{"class":215},[203,27632,257],{"class":219},[203,27634,27635],{"class":205,"line":278},[203,27636,757],{"emptyLinePlaceholder":756},[203,27638,27639,27641,27643],{"class":205,"line":285},[203,27640,17969],{"class":209},[203,27642,27607],{"class":215},[203,27644,18242],{"class":219},[203,27646,27647],{"class":205,"line":541},[203,27648,751],{"class":219},[203,27650,27651],{"class":205,"line":549},[203,27652,757],{"emptyLinePlaceholder":756},[203,27654,27655,27657,27659,27662,27665],{"class":205,"line":787},[203,27656,1555],{"class":209},[203,27658,17972],{"class":209},[203,27660,27661],{"class":219}," Singleton ",[203,27663,27664],{"class":215},"getInstance",[203,27666,18242],{"class":219},[203,27668,27669,27671],{"class":205,"line":799},[203,27670,18123],{"class":209},[203,27672,27673],{"class":219}," INSTANCE;\n",[203,27675,27676],{"class":205,"line":1371},[203,27677,751],{"class":219},[203,27679,27680],{"class":205,"line":17966},[203,27681,288],{"class":219},[18,27683,27684,27685,27688],{},"它的线程安全来自 JVM 的类初始化机制。静态字段的赋值会进入类初始化方法 ",[22,27686,27687],{},"\u003Cclinit>()","，可以近似理解为：",[144,27690,27692],{"className":197,"code":27691,"language":199,"meta":150,"style":150},"static {\n    INSTANCE = new Singleton();\n}\n",[22,27693,27694,27701,27714],{"__ignoreMap":150},[203,27695,27696,27699],{"class":205,"line":206},[203,27697,27698],{"class":209},"static",[203,27700,248],{"class":219},[203,27702,27703,27706,27708,27710,27712],{"class":205,"line":251},[203,27704,27705],{"class":219},"    INSTANCE ",[203,27707,495],{"class":209},[203,27709,212],{"class":209},[203,27711,27607],{"class":215},[203,27713,257],{"class":219},[203,27715,27716],{"class":205,"line":260},[203,27717,288],{"class":219},[18,27719,27720,27721,27723],{},"JVM 会同步一个类的初始化过程：同一个 ClassLoader 下，只有一个线程能够执行该类的 ",[22,27722,27687],{},"；其他首次使用该类的线程需要等待初始化完成。类初始化完成后，初始化过程中的写入 happens-before 其他线程后续对该类的使用。",[144,27725,27728],{"className":27726,"code":27727,"language":149,"meta":150},[147],"线程 A 首次主动使用 Singleton\n        ↓\n获得 Singleton 的类初始化权\n        ↓\n执行 \u003Cclinit>()，创建并初始化对象\n        ↓\n将引用赋给 INSTANCE\n        ↓\nSingleton 类初始化完成\n        ↓\n等待中的线程才能继续读取 INSTANCE\n",[22,27729,27727],{"__ignoreMap":150},[18,27731,27732,27733,27735,27736,27739],{},"创建对象可以概念性地分为“分配内存、执行构造方法、发布引用”。JVM 和处理器可以进行不改变程序语义的优化，但类初始化机制保证其他线程不能在 ",[22,27734,27687],{}," 尚未完成时，把这个类当作已经初始化完成来使用。因此，其他线程不会合法地观察到“",[22,27737,27738],{},"INSTANCE"," 已经非空，但构造结果还不可见”的状态。",[18,27741,8521,27742,27744,27745,27747,27748,33],{},[22,27743,25884],{}," 主要用于防止 ",[22,27746,27738],{}," 后续重新指向其他对象；饿汉式的只初始化一次和安全发布，主要由 JVM 的类初始化机制保证，而不是单独依赖 ",[22,27749,25884],{},[18,27751,27752],{},"优点：",[159,27754,27755,27758,27761],{},[162,27756,27757],{},"实现简单；",[162,27759,27760],{},"天然线程安全；",[162,27762,27763],{},"实例通过类初始化安全发布，不需要额外加锁。",[18,27765,27766],{},"缺点：",[159,27768,27769,27772,27775],{},[162,27770,27771],{},"类被初始化时就会创建实例；",[162,27773,27774],{},"如果实例创建成本较高且最终没有使用，会提前占用资源；",[162,27776,27777],{},"构造失败会导致类初始化失败。",[18,27779,27780],{},"适合对象创建成本不高、启动时即可创建的场景。",[14,27782,27784],{"id":27783},"三静态内部类","三、静态内部类",[18,27786,27787],{},"静态内部类可以同时实现线程安全与懒加载：",[144,27789,27791],{"className":197,"code":27790,"language":199,"meta":150,"style":150},"public final class Singleton {\n\n    private Singleton() {\n    }\n\n    private static class Holder {\n        private static final Singleton INSTANCE = new Singleton();\n    }\n\n    public static Singleton getInstance() {\n        return Holder.INSTANCE;\n    }\n}\n",[22,27792,27793,27805,27809,27817,27821,27825,27838,27857,27861,27865,27877,27884,27888],{"__ignoreMap":150},[203,27794,27795,27797,27799,27801,27803],{"class":205,"line":206},[203,27796,441],{"class":209},[203,27798,17952],{"class":209},[203,27800,1536],{"class":209},[203,27802,27607],{"class":215},[203,27804,248],{"class":219},[203,27806,27807],{"class":205,"line":251},[203,27808,757],{"emptyLinePlaceholder":756},[203,27810,27811,27813,27815],{"class":205,"line":260},[203,27812,17969],{"class":209},[203,27814,27607],{"class":215},[203,27816,18242],{"class":219},[203,27818,27819],{"class":205,"line":278},[203,27820,751],{"class":219},[203,27822,27823],{"class":205,"line":285},[203,27824,757],{"emptyLinePlaceholder":756},[203,27826,27827,27829,27831,27833,27836],{"class":205,"line":541},[203,27828,17969],{"class":209},[203,27830,17972],{"class":209},[203,27832,1536],{"class":209},[203,27834,27835],{"class":215}," Holder",[203,27837,248],{"class":219},[203,27839,27840,27843,27845,27847,27849,27851,27853,27855],{"class":205,"line":549},[203,27841,27842],{"class":209},"        private",[203,27844,17972],{"class":209},[203,27846,17952],{"class":209},[203,27848,27624],{"class":219},[203,27850,495],{"class":209},[203,27852,212],{"class":209},[203,27854,27607],{"class":215},[203,27856,257],{"class":219},[203,27858,27859],{"class":205,"line":787},[203,27860,751],{"class":219},[203,27862,27863],{"class":205,"line":799},[203,27864,757],{"emptyLinePlaceholder":756},[203,27866,27867,27869,27871,27873,27875],{"class":205,"line":1371},[203,27868,1555],{"class":209},[203,27870,17972],{"class":209},[203,27872,27661],{"class":219},[203,27874,27664],{"class":215},[203,27876,18242],{"class":219},[203,27878,27879,27881],{"class":205,"line":17966},[203,27880,18123],{"class":209},[203,27882,27883],{"class":219}," Holder.INSTANCE;\n",[203,27885,27886],{"class":205,"line":17989},[203,27887,751],{"class":219},[203,27889,27890],{"class":205,"line":17994},[203,27891,288],{"class":219},[18,27893,27894,27895,27898,27899,27902],{},"虽然 ",[22,27896,27897],{},"Holder"," 写在 ",[22,27900,27901],{},"Singleton"," 内部，但编译后它们是两个独立的类：",[144,27904,27907],{"className":27905,"code":27906,"language":149,"meta":150},[147],"Singleton.class\nSingleton$Holder.class\n",[22,27908,27906],{"__ignoreMap":150},[18,27910,27911,27912,27914,27915,27917,27918,27921,27922,27925,27926,27928,27929,27932],{},"初始化外部类 ",[22,27913,27901],{},"，不会因此立即初始化内部类 ",[22,27916,27897],{},"。第一次调用 ",[22,27919,27920],{},"getInstance()"," 并读取 ",[22,27923,27924],{},"Holder.INSTANCE"," 属于对 ",[22,27927,27897],{}," 的主动使用，此时 JVM 才执行 ",[22,27930,27931],{},"Holder.\u003Cclinit>()"," 并创建实例。",[144,27934,27937],{"className":27935,"code":27936,"language":149,"meta":150},[147],"加载 Singleton\n    ↓\n尚未初始化 Holder，不创建实例\n    ↓\n第一次调用 getInstance()\n    ↓\nJVM 初始化 Holder\n    ↓\n创建并安全发布 INSTANCE\n",[22,27938,27936],{"__ignoreMap":150},[18,27940,27941,27942,27944,27945,27947],{},"如果多个线程同时第一次调用 ",[22,27943,27920],{},"，它们可能同时执行到读取 ",[22,27946,27924],{},"，但不会分别创建对象：",[144,27949,27952],{"className":27950,"code":27951,"language":149,"meta":150},[147],"线程 A ──→ 获得 Holder 的类初始化权\n线程 B ──→ 等待 Holder 初始化完成\n线程 C ──→ 等待 Holder 初始化完成\n\n线程 A 执行 Holder.\u003Cclinit>()\n        ↓\n创建 Singleton 并赋值给 INSTANCE\n        ↓\nHolder 初始化完成\n        ↓\n线程 B、C 继续执行并读取同一个 INSTANCE\n",[22,27953,27951],{"__ignoreMap":150},[18,27955,27956,27957,27959,27960,27962,27963,33],{},"它的懒加载来自“内部类到首次主动使用时才初始化”，线程安全和可见性则来自 JVM 对 ",[22,27958,27897],{}," 类初始化过程的同步与 happens-before 保证。因此这里不需要再使用 ",[22,27961,27531],{}," 或显式的 ",[22,27964,2172],{},[18,27966,27967],{},"这种写法读取实例时没有业务代码中的锁竞争，通常是普通业务代码中实现懒加载单例的优先选择。",[14,27969,27971],{"id":27970},"四双重检查锁","四、双重检查锁",[18,27973,27974],{},"双重检查锁也称 DCL，即 Double-Checked Locking：",[144,27976,27978],{"className":197,"code":27977,"language":199,"meta":150,"style":150},"public final class Singleton {\n\n    private static volatile Singleton instance;\n\n    private Singleton() {\n    }\n\n    public static Singleton getInstance() {\n        if (instance == null) {\n            synchronized (Singleton.class) {\n                if (instance == null) {\n                    instance = new Singleton();\n                }\n            }\n        }\n\n        return instance;\n    }\n}\n",[22,27979,27980,27992,27996,28007,28011,28019,28023,28027,28039,28052,28060,28072,28085,28089,28093,28097,28101,28108,28112],{"__ignoreMap":150},[203,27981,27982,27984,27986,27988,27990],{"class":205,"line":206},[203,27983,441],{"class":209},[203,27985,17952],{"class":209},[203,27987,1536],{"class":209},[203,27989,27607],{"class":215},[203,27991,248],{"class":219},[203,27993,27994],{"class":205,"line":251},[203,27995,757],{"emptyLinePlaceholder":756},[203,27997,27998,28000,28002,28004],{"class":205,"line":260},[203,27999,17969],{"class":209},[203,28001,17972],{"class":209},[203,28003,25994],{"class":209},[203,28005,28006],{"class":219}," Singleton instance;\n",[203,28008,28009],{"class":205,"line":278},[203,28010,757],{"emptyLinePlaceholder":756},[203,28012,28013,28015,28017],{"class":205,"line":285},[203,28014,17969],{"class":209},[203,28016,27607],{"class":215},[203,28018,18242],{"class":219},[203,28020,28021],{"class":205,"line":541},[203,28022,751],{"class":219},[203,28024,28025],{"class":205,"line":549},[203,28026,757],{"emptyLinePlaceholder":756},[203,28028,28029,28031,28033,28035,28037],{"class":205,"line":787},[203,28030,1555],{"class":209},[203,28032,17972],{"class":209},[203,28034,27661],{"class":219},[203,28036,27664],{"class":215},[203,28038,18242],{"class":219},[203,28040,28041,28043,28046,28048,28050],{"class":205,"line":799},[203,28042,20089],{"class":209},[203,28044,28045],{"class":219}," (instance ",[203,28047,715],{"class":209},[203,28049,719],{"class":718},[203,28051,275],{"class":219},[203,28053,28054,28057],{"class":205,"line":1371},[203,28055,28056],{"class":209},"            synchronized",[203,28058,28059],{"class":219}," (Singleton.class) {\n",[203,28061,28062,28064,28066,28068,28070],{"class":205,"line":17966},[203,28063,18419],{"class":209},[203,28065,28045],{"class":219},[203,28067,715],{"class":209},[203,28069,719],{"class":718},[203,28071,275],{"class":219},[203,28073,28074,28077,28079,28081,28083],{"class":205,"line":17989},[203,28075,28076],{"class":219},"                    instance ",[203,28078,495],{"class":209},[203,28080,212],{"class":209},[203,28082,27607],{"class":215},[203,28084,257],{"class":219},[203,28086,28087],{"class":205,"line":17994},[203,28088,18469],{"class":219},[203,28090,28091],{"class":205,"line":18015},[203,28092,18513],{"class":219},[203,28094,28095],{"class":205,"line":18030},[203,28096,18326],{"class":219},[203,28098,28099],{"class":205,"line":18036},[203,28100,757],{"emptyLinePlaceholder":756},[203,28102,28103,28105],{"class":205,"line":18053},[203,28104,18123],{"class":209},[203,28106,28107],{"class":219}," instance;\n",[203,28109,28110],{"class":205,"line":18059},[203,28111,751],{"class":219},[203,28113,28114],{"class":205,"line":18073},[203,28115,288],{"class":219},[473,28117,28119],{"id":28118},"_1-为什么检查两次","1. 为什么检查两次",[18,28121,28122],{},"第一次检查发生在同步块外：",[144,28124,28126],{"className":197,"code":28125,"language":199,"meta":150,"style":150},"if (result == null)\n",[22,28127,28128],{"__ignoreMap":150},[203,28129,28130,28133,28136,28138,28140],{"class":205,"line":206},[203,28131,28132],{"class":209},"if",[203,28134,28135],{"class":219}," (result ",[203,28137,715],{"class":209},[203,28139,719],{"class":718},[203,28141,12343],{"class":219},[18,28143,28144],{},"实例已经创建后，绝大多数调用可以直接返回，不必再竞争锁。",[18,28146,28147],{},"第二次检查发生在同步块内：",[144,28149,28151],{"className":197,"code":28150,"language":199,"meta":150,"style":150},"synchronized (Singleton.class) {\n    result = instance;\n    if (result == null) {\n        \u002F\u002F 创建实例\n    }\n}\n",[22,28152,28153,28159,28168,28180,28185,28189],{"__ignoreMap":150},[203,28154,28155,28157],{"class":205,"line":206},[203,28156,2172],{"class":209},[203,28158,28059],{"class":219},[203,28160,28161,28164,28166],{"class":205,"line":251},[203,28162,28163],{"class":219},"    result ",[203,28165,495],{"class":209},[203,28167,28107],{"class":219},[203,28169,28170,28172,28174,28176,28178],{"class":205,"line":260},[203,28171,709],{"class":209},[203,28173,28135],{"class":219},[203,28175,715],{"class":209},[203,28177,719],{"class":718},[203,28179,275],{"class":219},[203,28181,28182],{"class":205,"line":278},[203,28183,28184],{"class":281},"        \u002F\u002F 创建实例\n",[203,28186,28187],{"class":205,"line":285},[203,28188,751],{"class":219},[203,28190,28191],{"class":205,"line":541},[203,28192,288],{"class":219},[18,28194,28195],{},"假设线程 A 和线程 B 同时通过第一次检查，线程 A 先获得锁并创建对象。线程 B 随后获得锁时，必须再次检查，否则还会创建第二个对象。",[473,28197,28199],{"id":28198},"_2-为什么必须使用-volatile","2. 为什么必须使用 volatile",[18,28201,28202],{},"创建对象可以概念性地分为：",[144,28204,28207],{"className":28205,"code":28206,"language":149,"meta":150},[147],"1. 分配内存\n2. 初始化对象\n3. 将引用赋给 instance\n",[22,28208,28206],{"__ignoreMap":150},[18,28210,28211],{},"如果没有适当的内存语义约束，其他线程可能观察到非空引用，却没有可靠地观察到对象完整初始化后的状态。",[144,28213,28215],{"className":197,"code":28214,"language":199,"meta":150,"style":150},"private static volatile Singleton instance;\n",[22,28216,28217],{"__ignoreMap":150},[203,28218,28219,28221,28223,28225],{"class":205,"line":206},[203,28220,25991],{"class":209},[203,28222,17972],{"class":209},[203,28224,25994],{"class":209},[203,28226,28006],{"class":219},[18,28228,28229,28231],{},[22,28230,27531],{}," 在这里有两个关键作用：",[159,28233,28234,28237],{},[162,28235,28236],{},"禁止对象初始化与引用发布之间发生不安全的重排序；",[162,28238,28239],{},"建立 volatile 写与后续 volatile 读之间的 happens-before 关系，使初始化结果对读取线程可见。",[14,28241,28243],{"id":28242},"五枚举单例","五、枚举单例",[18,28245,28246],{},"枚举是更简洁、约束更强的单例实现：",[144,28248,28250],{"className":197,"code":28249,"language":199,"meta":150,"style":150},"public enum Singleton {\n    INSTANCE(\"default-service\", 3000);\n\n    private final String serviceName;\n    private final int timeoutMillis;\n\n    Singleton(String serviceName, int timeoutMillis) {\n        this.serviceName = serviceName;\n        this.timeoutMillis = timeoutMillis;\n    }\n\n    public String getServiceName() {\n        return serviceName;\n    }\n\n    public int getTimeoutMillis() {\n        return timeoutMillis;\n    }\n\n    public void execute() {\n        System.out.println(\n                serviceName + \" timeout=\" + timeoutMillis);\n    }\n}\n",[22,28251,28252,28263,28280,28284,28293,28304,28308,28327,28339,28350,28354,28358,28369,28375,28379,28383,28394,28400,28404,28408,28418,28427,28442,28446],{"__ignoreMap":150},[203,28253,28254,28256,28259,28261],{"class":205,"line":206},[203,28255,441],{"class":209},[203,28257,28258],{"class":209}," enum",[203,28260,27607],{"class":215},[203,28262,248],{"class":219},[203,28264,28265,28268,28270,28273,28275,28278],{"class":205,"line":251},[203,28266,28267],{"class":718},"    INSTANCE",[203,28269,220],{"class":219},[203,28271,28272],{"class":223},"\"default-service\"",[203,28274,5179],{"class":219},[203,28276,28277],{"class":718},"3000",[203,28279,227],{"class":219},[203,28281,28282],{"class":205,"line":260},[203,28283,757],{"emptyLinePlaceholder":756},[203,28285,28286,28288,28290],{"class":205,"line":278},[203,28287,17969],{"class":209},[203,28289,17952],{"class":209},[203,28291,28292],{"class":219}," String serviceName;\n",[203,28294,28295,28297,28299,28301],{"class":205,"line":285},[203,28296,17969],{"class":209},[203,28298,17952],{"class":209},[203,28300,25909],{"class":209},[203,28302,28303],{"class":219}," timeoutMillis;\n",[203,28305,28306],{"class":205,"line":541},[203,28307,757],{"emptyLinePlaceholder":756},[203,28309,28310,28313,28315,28318,28320,28322,28325],{"class":205,"line":549},[203,28311,28312],{"class":215},"    Singleton",[203,28314,1688],{"class":219},[203,28316,28317],{"class":271},"serviceName",[203,28319,5179],{"class":219},[203,28321,25932],{"class":209},[203,28323,28324],{"class":271}," timeoutMillis",[203,28326,275],{"class":219},[203,28328,28329,28331,28334,28336],{"class":205,"line":787},[203,28330,25942],{"class":718},[203,28332,28333],{"class":219},".serviceName ",[203,28335,495],{"class":209},[203,28337,28338],{"class":219}," serviceName;\n",[203,28340,28341,28343,28346,28348],{"class":205,"line":799},[203,28342,25942],{"class":718},[203,28344,28345],{"class":219},".timeoutMillis ",[203,28347,495],{"class":209},[203,28349,28303],{"class":219},[203,28351,28352],{"class":205,"line":1371},[203,28353,751],{"class":219},[203,28355,28356],{"class":205,"line":17966},[203,28357,757],{"emptyLinePlaceholder":756},[203,28359,28360,28362,28364,28367],{"class":205,"line":17989},[203,28361,1555],{"class":209},[203,28363,444],{"class":219},[203,28365,28366],{"class":215},"getServiceName",[203,28368,18242],{"class":219},[203,28370,28371,28373],{"class":205,"line":17994},[203,28372,18123],{"class":209},[203,28374,28338],{"class":219},[203,28376,28377],{"class":205,"line":18015},[203,28378,751],{"class":219},[203,28380,28381],{"class":205,"line":18030},[203,28382,757],{"emptyLinePlaceholder":756},[203,28384,28385,28387,28389,28392],{"class":205,"line":18036},[203,28386,1555],{"class":209},[203,28388,25909],{"class":209},[203,28390,28391],{"class":215}," getTimeoutMillis",[203,28393,18242],{"class":219},[203,28395,28396,28398],{"class":205,"line":18053},[203,28397,18123],{"class":209},[203,28399,28303],{"class":219},[203,28401,28402],{"class":205,"line":18059},[203,28403,751],{"class":219},[203,28405,28406],{"class":205,"line":18073},[203,28407,757],{"emptyLinePlaceholder":756},[203,28409,28410,28412,28414,28416],{"class":205,"line":18083},[203,28411,1555],{"class":209},[203,28413,567],{"class":209},[203,28415,18264],{"class":215},[203,28417,18242],{"class":219},[203,28419,28420,28423,28425],{"class":205,"line":18109},[203,28421,28422],{"class":219},"        System.out.",[203,28424,3399],{"class":215},[203,28426,1496],{"class":219},[203,28428,28429,28432,28434,28437,28439],{"class":205,"line":18115},[203,28430,28431],{"class":219},"                serviceName ",[203,28433,11953],{"class":209},[203,28435,28436],{"class":223}," \" timeout=\"",[203,28438,1504],{"class":209},[203,28440,28441],{"class":219}," timeoutMillis);\n",[203,28443,28444],{"class":205,"line":18120},[203,28445,751],{"class":219},[203,28447,28448],{"class":205,"line":18129},[203,28449,288],{"class":219},[18,28451,28452],{},"调用方式：",[144,28454,28456],{"className":197,"code":28455,"language":199,"meta":150,"style":150},"Singleton.INSTANCE.execute();\n\nString serviceName = Singleton.INSTANCE.getServiceName();\nint timeoutMillis = Singleton.INSTANCE.getTimeoutMillis();\n",[22,28457,28458,28467,28471,28485],{"__ignoreMap":150},[203,28459,28460,28463,28465],{"class":205,"line":206},[203,28461,28462],{"class":219},"Singleton.INSTANCE.",[203,28464,17721],{"class":215},[203,28466,257],{"class":219},[203,28468,28469],{"class":205,"line":251},[203,28470,757],{"emptyLinePlaceholder":756},[203,28472,28473,28476,28478,28481,28483],{"class":205,"line":260},[203,28474,28475],{"class":219},"String serviceName ",[203,28477,495],{"class":209},[203,28479,28480],{"class":219}," Singleton.INSTANCE.",[203,28482,28366],{"class":215},[203,28484,257],{"class":219},[203,28486,28487,28489,28492,28494,28496,28499],{"class":205,"line":278},[203,28488,25932],{"class":209},[203,28490,28491],{"class":219}," timeoutMillis ",[203,28493,495],{"class":209},[203,28495,28480],{"class":219},[203,28497,28498],{"class":215},"getTimeoutMillis",[203,28500,257],{"class":219},[18,28502,28503,28504,28506],{},"枚举常量可以拥有构造参数、成员属性和普通方法。上例中的属性使用 ",[22,28505,25884],{}," 修饰，实例创建后不再改变，多个线程读取时不需要额外同步。如果枚举单例内部保存的是可变属性，并发访问时仍然需要锁或其他线程安全机制。",[18,28508,28509],{},"枚举单例具有以下特点：",[159,28511,28512,28515,28518,28521],{},[162,28513,28514],{},"JVM 保证枚举常量只初始化一次；",[162,28516,28517],{},"枚举构造器不能通过常规反射方式创建新实例；",[162,28519,28520],{},"Java 序列化机制能够保持枚举常量的唯一性；",[162,28522,28523],{},"代码简洁，不需要手动编写并发控制。",[18,28525,28526],{},"如果业务能够接受枚举形式，并且特别关注反射与反序列化对单例的破坏，枚举通常是更稳妥的选择。",[14,28528,28530],{"id":28529},"六同步方法实现","六、同步方法实现",[18,28532,28533],{},"最直接的懒加载写法是给整个获取方法加锁：",[144,28535,28537],{"className":197,"code":28536,"language":199,"meta":150,"style":150},"public final class Singleton {\n\n    private static Singleton instance;\n\n    private Singleton() {\n    }\n\n    public static synchronized Singleton getInstance() {\n        if (instance == null) {\n            instance = new Singleton();\n        }\n        return instance;\n    }\n}\n",[22,28538,28539,28551,28555,28563,28567,28575,28579,28583,28598,28610,28623,28627,28633,28637],{"__ignoreMap":150},[203,28540,28541,28543,28545,28547,28549],{"class":205,"line":206},[203,28542,441],{"class":209},[203,28544,17952],{"class":209},[203,28546,1536],{"class":209},[203,28548,27607],{"class":215},[203,28550,248],{"class":219},[203,28552,28553],{"class":205,"line":251},[203,28554,757],{"emptyLinePlaceholder":756},[203,28556,28557,28559,28561],{"class":205,"line":260},[203,28558,17969],{"class":209},[203,28560,17972],{"class":209},[203,28562,28006],{"class":219},[203,28564,28565],{"class":205,"line":278},[203,28566,757],{"emptyLinePlaceholder":756},[203,28568,28569,28571,28573],{"class":205,"line":285},[203,28570,17969],{"class":209},[203,28572,27607],{"class":215},[203,28574,18242],{"class":219},[203,28576,28577],{"class":205,"line":541},[203,28578,751],{"class":219},[203,28580,28581],{"class":205,"line":549},[203,28582,757],{"emptyLinePlaceholder":756},[203,28584,28585,28587,28589,28592,28594,28596],{"class":205,"line":787},[203,28586,1555],{"class":209},[203,28588,17972],{"class":209},[203,28590,28591],{"class":209}," synchronized",[203,28593,27661],{"class":219},[203,28595,27664],{"class":215},[203,28597,18242],{"class":219},[203,28599,28600,28602,28604,28606,28608],{"class":205,"line":799},[203,28601,20089],{"class":209},[203,28603,28045],{"class":219},[203,28605,715],{"class":209},[203,28607,719],{"class":718},[203,28609,275],{"class":219},[203,28611,28612,28615,28617,28619,28621],{"class":205,"line":1371},[203,28613,28614],{"class":219},"            instance ",[203,28616,495],{"class":209},[203,28618,212],{"class":209},[203,28620,27607],{"class":215},[203,28622,257],{"class":219},[203,28624,28625],{"class":205,"line":17966},[203,28626,18326],{"class":219},[203,28628,28629,28631],{"class":205,"line":17989},[203,28630,18123],{"class":209},[203,28632,28107],{"class":219},[203,28634,28635],{"class":205,"line":17994},[203,28636,751],{"class":219},[203,28638,28639],{"class":205,"line":18015},[203,28640,288],{"class":219},[18,28642,28643,28644,28646],{},"这种实现是线程安全的，但每次调用 ",[22,28645,27920],{}," 都要进入同步方法。现代 JVM 对无竞争锁已有较多优化，但在能够使用静态内部类的情况下，通常没有必要让每次读取都经过同步。",[18,28648,28649],{},"它的优点是逻辑直观，适合教学或对性能不敏感的简单场景。",[14,28651,28653],{"id":28652},"七两种错误实现","七、两种错误实现",[473,28655,28657],{"id":28656},"_1-没有同步的懒加载","1. 没有同步的懒加载",[144,28659,28661],{"className":197,"code":28660,"language":199,"meta":150,"style":150},"public static Singleton getInstance() {\n    if (instance == null) {\n        instance = new Singleton();\n    }\n    return instance;\n}\n",[22,28662,28663,28675,28687,28700,28704,28710],{"__ignoreMap":150},[203,28664,28665,28667,28669,28671,28673],{"class":205,"line":206},[203,28666,441],{"class":209},[203,28668,17972],{"class":209},[203,28670,27661],{"class":219},[203,28672,27664],{"class":215},[203,28674,18242],{"class":219},[203,28676,28677,28679,28681,28683,28685],{"class":205,"line":251},[203,28678,709],{"class":209},[203,28680,28045],{"class":219},[203,28682,715],{"class":209},[203,28684,719],{"class":718},[203,28686,275],{"class":219},[203,28688,28689,28692,28694,28696,28698],{"class":205,"line":260},[203,28690,28691],{"class":219},"        instance ",[203,28693,495],{"class":209},[203,28695,212],{"class":209},[203,28697,27607],{"class":215},[203,28699,257],{"class":219},[203,28701,28702],{"class":205,"line":278},[203,28703,751],{"class":219},[203,28705,28706,28708],{"class":205,"line":285},[203,28707,455],{"class":209},[203,28709,28107],{"class":219},[203,28711,28712],{"class":205,"line":541},[203,28713,288],{"class":219},[18,28715,28716,28717,28720],{},"多个线程可能同时判断 ",[22,28718,28719],{},"instance == null","，随后分别创建对象，因此不能保证单例。",[473,28722,28724],{"id":28723},"_2-缺少第二次检查","2. 缺少第二次检查",[144,28726,28728],{"className":197,"code":28727,"language":199,"meta":150,"style":150},"public static Singleton getInstance() {\n    if (instance == null) {\n        synchronized (Singleton.class) {\n            instance = new Singleton();\n        }\n    }\n    return instance;\n}\n",[22,28729,28730,28742,28754,28761,28773,28777,28781,28787],{"__ignoreMap":150},[203,28731,28732,28734,28736,28738,28740],{"class":205,"line":206},[203,28733,441],{"class":209},[203,28735,17972],{"class":209},[203,28737,27661],{"class":219},[203,28739,27664],{"class":215},[203,28741,18242],{"class":219},[203,28743,28744,28746,28748,28750,28752],{"class":205,"line":251},[203,28745,709],{"class":209},[203,28747,28045],{"class":219},[203,28749,715],{"class":209},[203,28751,719],{"class":718},[203,28753,275],{"class":219},[203,28755,28756,28759],{"class":205,"line":260},[203,28757,28758],{"class":209},"        synchronized",[203,28760,28059],{"class":219},[203,28762,28763,28765,28767,28769,28771],{"class":205,"line":278},[203,28764,28614],{"class":219},[203,28766,495],{"class":209},[203,28768,212],{"class":209},[203,28770,27607],{"class":215},[203,28772,257],{"class":219},[203,28774,28775],{"class":205,"line":285},[203,28776,18326],{"class":219},[203,28778,28779],{"class":205,"line":541},[203,28780,751],{"class":219},[203,28782,28783,28785],{"class":205,"line":549},[203,28784,455],{"class":209},[203,28786,28107],{"class":219},[203,28788,28789],{"class":205,"line":787},[203,28790,288],{"class":219},[18,28792,28793],{},"多个线程可能同时通过外层判断，然后依次进入同步块并各自创建对象。加锁只能保证它们不会同时创建，不能阻止后获得锁的线程再次创建，因此锁内必须重新判断。",[14,28795,28797],{"id":28796},"八单例可能被哪些方式破坏","八、单例可能被哪些方式破坏",[473,28799,28801],{"id":28800},"_1-反射","1. 反射",[18,28803,28804],{},"普通单例的私有构造器仍可能被反射调用：",[144,28806,28808],{"className":197,"code":28807,"language":199,"meta":150,"style":150},"Constructor\u003CSingleton> constructor =\n        Singleton.class.getDeclaredConstructor();\nconstructor.setAccessible(true);\nSingleton another = constructor.newInstance();\n",[22,28809,28810,28822,28832,28846],{"__ignoreMap":150},[203,28811,28812,28815,28817,28820],{"class":205,"line":206},[203,28813,28814],{"class":219},"Constructor\u003C",[203,28816,27901],{"class":209},[203,28818,28819],{"class":219},"> constructor ",[203,28821,16382],{"class":209},[203,28823,28824,28827,28830],{"class":205,"line":251},[203,28825,28826],{"class":219},"        Singleton.class.",[203,28828,28829],{"class":215},"getDeclaredConstructor",[203,28831,257],{"class":219},[203,28833,28834,28837,28840,28842,28844],{"class":205,"line":260},[203,28835,28836],{"class":219},"constructor.",[203,28838,28839],{"class":215},"setAccessible",[203,28841,220],{"class":219},[203,28843,5166],{"class":718},[203,28845,227],{"class":219},[203,28847,28848,28851,28853,28856,28859],{"class":205,"line":278},[203,28849,28850],{"class":219},"Singleton another ",[203,28852,495],{"class":209},[203,28854,28855],{"class":219}," constructor.",[203,28857,28858],{"class":215},"newInstance",[203,28860,257],{"class":219},[18,28862,28863],{},"可以在构造器中增加防御性检查，但这种方式需要谨慎处理初始化顺序，也不能替代可靠的访问控制。枚举单例能够从语言与 JVM 层面阻止这种常规反射创建。",[473,28865,28867],{"id":28866},"_2-反序列化","2. 反序列化",[18,28869,28870,28871,28874],{},"普通单例实现 ",[22,28872,28873],{},"Serializable"," 后，反序列化默认可能创建新对象。可以定义：",[144,28876,28878],{"className":197,"code":28877,"language":199,"meta":150,"style":150},"private Object readResolve() {\n    return INSTANCE;\n}\n",[22,28879,28880,28891,28897],{"__ignoreMap":150},[203,28881,28882,28884,28886,28889],{"class":205,"line":206},[203,28883,25991],{"class":209},[203,28885,19744],{"class":219},[203,28887,28888],{"class":215},"readResolve",[203,28890,18242],{"class":219},[203,28892,28893,28895],{"class":205,"line":251},[203,28894,455],{"class":209},[203,28896,27673],{"class":219},[203,28898,28899],{"class":205,"line":260},[203,28900,288],{"class":219},[18,28902,28903],{},"让反序列化结果返回已有实例。枚举单例由序列化机制直接保证唯一性。",[473,28905,28907],{"id":28906},"_3-克隆","3. 克隆",[18,28909,28910,28911,28914,28915,28918,28919,28921],{},"如果单例支持 ",[22,28912,28913],{},"Cloneable"," 并允许调用 ",[22,28916,28917],{},"clone()","，也可能产生新对象。单例类通常不应实现 ",[22,28920,28913],{},"，或者应拒绝克隆。",[473,28923,28925],{"id":28924},"_4-多个-classloader","4. 多个 ClassLoader",[18,28927,28928],{},"静态字段属于其对应的类，而类的身份由“类名 + 加载它的 ClassLoader”共同决定。同一个类被不同 ClassLoader 分别加载时，每个 ClassLoader 都可以拥有自己的单例实例。",[18,28930,28931],{},"因此 Java 单例通常保证的是：",[105,28933,28934],{},[18,28935,28936],{},"同一个 JVM、同一个 ClassLoader 范围内只有一个实例。",[18,28938,28939],{},"它不能保证多个 JVM、多个容器实例或分布式集群中只有一个对象。",[14,28941,28943],{"id":28942},"九不同实现如何选择","九、不同实现如何选择",[299,28945,28946,28965],{},[302,28947,28948],{},[305,28949,28950,28953,28956,28959,28962],{},[308,28951,28952],{},"实现方式",[308,28954,28955],{"align":8425},"线程安全",[308,28957,28958],{"align":8425},"懒加载",[308,28960,28961],{},"主要特点",[308,28963,28964],{},"推荐场景",[315,28966,28967,28982,28997,29014,29030],{},[305,28968,28969,28972,28974,28976,28979],{},[320,28970,28971],{},"饿汉式",[320,28973,981],{"align":8425},[320,28975,984],{"align":8425},[320,28977,28978],{},"简单，依靠类初始化",[320,28980,28981],{},"创建成本低、启动时即可创建",[305,28983,28984,28987,28989,28991,28994],{},[320,28985,28986],{},"静态内部类",[320,28988,981],{"align":8425},[320,28990,981],{"align":8425},[320,28992,28993],{},"无显式锁，读取开销低",[320,28995,28996],{},"普通业务中的懒加载单例",[305,28998,28999,29002,29004,29006,29011],{},[320,29000,29001],{},"双重检查锁",[320,29003,981],{"align":8425},[320,29005,981],{"align":8425},[320,29007,29008,29009],{},"必须正确使用 ",[22,29010,27531],{},[320,29012,29013],{},"需要显式控制初始化过程",[305,29015,29016,29019,29021,29024,29027],{},[320,29017,29018],{},"枚举",[320,29020,981],{"align":8425},[320,29022,29023],{"align":8425},"通常不作为延迟加载方案",[320,29025,29026],{},"防常规反射和反序列化破坏",[320,29028,29029],{},"强调实现简洁和唯一性",[305,29031,29032,29035,29037,29039,29042],{},[320,29033,29034],{},"同步方法",[320,29036,981],{"align":8425},[320,29038,981],{"align":8425},[320,29040,29041],{},"简单但每次获取都同步",[320,29043,29044],{},"教学或低频简单场景",[14,29046,29048],{"id":29047},"十单例不等于无条件线程安全","十、单例不等于无条件线程安全",[18,29050,29051],{},"单例模式只保证实例数量，不会自动保证实例内部状态的线程安全。",[144,29053,29055],{"className":197,"code":29054,"language":199,"meta":150,"style":150},"public final class CounterSingleton {\n\n    private int count;\n\n    public void increment() {\n        count++;\n    }\n}\n",[22,29056,29057,29070,29074,29083,29087,29098,29108,29112],{"__ignoreMap":150},[203,29058,29059,29061,29063,29065,29068],{"class":205,"line":206},[203,29060,441],{"class":209},[203,29062,17952],{"class":209},[203,29064,1536],{"class":209},[203,29066,29067],{"class":215}," CounterSingleton",[203,29069,248],{"class":219},[203,29071,29072],{"class":205,"line":251},[203,29073,757],{"emptyLinePlaceholder":756},[203,29075,29076,29078,29080],{"class":205,"line":260},[203,29077,17969],{"class":209},[203,29079,25909],{"class":209},[203,29081,29082],{"class":219}," count;\n",[203,29084,29085],{"class":205,"line":278},[203,29086,757],{"emptyLinePlaceholder":756},[203,29088,29089,29091,29093,29096],{"class":205,"line":285},[203,29090,1555],{"class":209},[203,29092,567],{"class":209},[203,29094,29095],{"class":215}," increment",[203,29097,18242],{"class":219},[203,29099,29100,29103,29106],{"class":205,"line":541},[203,29101,29102],{"class":219},"        count",[203,29104,29105],{"class":209},"++",[203,29107,1297],{"class":219},[203,29109,29110],{"class":205,"line":549},[203,29111,751],{"class":219},[203,29113,29114],{"class":205,"line":787},[203,29115,288],{"class":219},[18,29117,13487,29118,29121,29122,29125],{},[22,29119,29120],{},"CounterSingleton"," 只有一个实例，多个线程并发执行 ",[22,29123,29124],{},"count++"," 仍然可能丢失更新。可变状态仍然需要使用锁、原子类、不可变对象或其他并发控制方式保护。",[18,29127,29128,29129,29131],{},"在 Spring 中，默认的单例 Bean 也只是表示一个 ",[22,29130,20673],{}," 中通常只有一个实例。Spring 不会自动保证这个 Bean 的可变字段线程安全。",[14,29133,29135],{"id":29134},"十一需要记住的边界","十一、需要记住的边界",[159,29137,29138,29141,29144,29149,29152,29155],{},[162,29139,29140],{},"单例保证实例数量，不保证内部状态线程安全；",[162,29142,29143],{},"Java 单例通常只在同一个 ClassLoader 范围内成立；",[162,29145,29146,29147,2365],{},"Spring Singleton 的作用域是一个 ",[22,29148,20673],{},[162,29150,29151],{},"多 JVM、多个服务实例之间的全局唯一，需要借助数据库唯一约束、分布式锁、协调服务或主节点选举等机制；",[162,29153,29154],{},"单例适合承载无状态逻辑，或者经过并发保护的共享状态；",[162,29156,29157],{},"不应把“当前用户”“本次请求参数”或普通可变集合等请求级数据保存在单例字段中。所有线程和测试用例会共享这些数据，容易造成请求间数据串扰、并发修改错误以及测试互相影响。",[2111,29159,29160],{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}",{"title":150,"searchDepth":251,"depth":251,"links":29162},[29163,29164,29165,29166,29167,29168,29169,29173,29174,29175,29179,29185,29186,29187],{"id":16,"depth":251,"text":16},{"id":115,"depth":251,"text":115},{"id":132,"depth":251,"text":132},{"id":27560,"depth":251,"text":27561},{"id":27587,"depth":251,"text":27588},{"id":27783,"depth":251,"text":27784},{"id":27970,"depth":251,"text":27971,"children":29170},[29171,29172],{"id":28118,"depth":260,"text":28119},{"id":28198,"depth":260,"text":28199},{"id":28242,"depth":251,"text":28243},{"id":28529,"depth":251,"text":28530},{"id":28652,"depth":251,"text":28653,"children":29176},[29177,29178],{"id":28656,"depth":260,"text":28657},{"id":28723,"depth":260,"text":28724},{"id":28796,"depth":251,"text":28797,"children":29180},[29181,29182,29183,29184],{"id":28800,"depth":260,"text":28801},{"id":28866,"depth":260,"text":28867},{"id":28906,"depth":260,"text":28907},{"id":28924,"depth":260,"text":28925},{"id":28942,"depth":251,"text":28943},{"id":29047,"depth":251,"text":29048},{"id":29134,"depth":251,"text":29135},"wiki:java:how-to-implement-singleton-pattern","从饿汉式、静态内部类、双重检查锁和枚举四种实现出发，讲清 Java 单例模式的线程安全、懒加载、安全发布及常见误区。",{},84,"\u002Fjava\u002Fhow-to-implement-singleton-pattern",{"title":27515,"description":29189},"java\u002Fhow-to-implement-singleton-pattern","2026-08-02","7stgbl5xJ-jqlkEvuYdhvc5JNhbN0piqO3YI_MJtApQ",{"id":29198,"title":29199,"body":29200,"commentId":31711,"description":31712,"difficulty":2147,"draft":2148,"extension":2149,"meta":31713,"navigation":756,"order":31714,"path":31715,"section":2153,"seo":31716,"stem":31717,"updated":2156,"__hash__":31718},"java\u002Fjava\u002Fspring-transaction-failure-scenarios.md","Spring 事务在什么情况下会失效？",{"type":7,"value":29201,"toc":31681},[29202,29205,29207,29218,29234,29237,29239,29246,29248,29257,29259,29263,29266,29272,29278,29281,29289,29292,29296,29299,29328,29337,29387,29390,29443,29446,29450,29453,29533,29548,29564,29570,29576,29579,29731,29738,29741,29882,29901,29904,29910,29954,29963,30064,30083,30086,30090,30093,30097,30122,30125,30129,30156,30162,30166,30193,30196,30200,30210,30215,30219,30225,30286,30301,30304,30310,30324,30331,30495,30506,30510,30513,30525,30528,30663,30676,30682,30810,30813,30824,30828,30831,30915,30918,30921,30958,30961,30990,30993,30997,31004,31073,31076,31081,31087,31090,31094,31097,31100,31182,31184,31230,31233,31236,31240,31243,31285,31288,31291,31307,31310,31314,31317,31334,31340,31343,31401,31404,31408,31414,31478,31481,31483,31487,31490,31504,31507,31511,31514,31520,31524,31526,31532,31535,31539,31542,31561,31563,31639,31641,31678],[10,29203,29199],{"id":29204},"spring-事务在什么情况下会失效",[14,29206,16],{"id":16},[18,29208,29209,29210,29213,29214,29217],{},"Spring 声明式事务主要通过 AOP 代理实现：外部调用先进入代理对象，",[22,29211,29212],{},"TransactionInterceptor"," 根据 ",[22,29215,29216],{},"@Transactional"," 配置开启事务，再调用目标方法，最后根据方法是否正常返回以及异常是否命中回滚规则决定提交或回滚。",[18,29219,29220,29221,29223,29224,29226,29227,53,29229,53,29231,29233],{},"因此，常见的事务失效场景可以分成两类。第一类是调用没有经过事务代理，例如对象不是 Spring Bean、同一个类中使用 ",[22,29222,2286],{}," 调用、在 ",[22,29225,19253],{}," 阶段调用事务方法，或者方法是代理无法增强的 ",[22,29228,25991],{},[22,29230,25884],{},[22,29232,27698],{}," 方法。第二类是事务已经创建，但行为与预期不同，例如默认不对受检异常回滚、异常被业务代码捕获后没有继续抛出、方法切换到新线程执行、事务传播行为选择不当、多数据源使用了错误的事务管理器，或者底层数据库操作本身不支持事务。",[18,29235,29236],{},"排查时先确认当前对象是不是代理对象、调用是否从代理外部进入，再确认事务是否真正开启，最后检查异常类型、传播行为、线程边界、事务管理器和数据库能力。",[18,29238,103],{},[105,29240,29241],{},[18,29242,29243],{},[110,29244,29245],{},"Spring 事务要生效，必须让调用经过正确的事务代理，并且异常、线程、传播行为、事务管理器和底层存储都处在同一个有效事务边界内。",[14,29247,115],{"id":115},[18,29249,29250],{},[119,29251,29254],{"href":29252,"target":122,"rel":29253},"\u002Fimages\u002Fwiki\u002Fjava\u002Fspring-transaction-failure-scenarios.svg",[124,125],[127,29255],{"src":29252,"alt":29256},"Spring 事务失效的常见原因和排查顺序",[14,29258,132],{"id":132},[14,29260,29262],{"id":29261},"一先理解-spring-事务为什么能够生效","一、先理解 Spring 事务为什么能够生效",[18,29264,29265],{},"在默认的代理模式下，Spring 会为符合条件的 Bean 创建事务代理。外部代码调用事务方法时，实际链路是：",[144,29267,29270],{"className":29268,"code":29269,"language":149,"meta":150},[147],"调用方\n  ↓\nSpring 事务代理\n  ↓\nTransactionInterceptor\n  ├─ 读取 @Transactional 属性\n  ├─ 选择 TransactionManager\n  ├─ 开启或加入事务\n  └─ 调用目标方法\n          ↓\n     正常返回：提交\n     命中回滚规则的异常：回滚\n",[22,29271,29269],{"__ignoreMap":150},[18,29273,29274,29275,29277],{},"所以，",[22,29276,29216],{}," 本身只是事务元数据。只有 Spring 的事务基础设施识别到它，并且方法调用经过代理，事务拦截器才有机会工作。",[18,29279,29280],{},"本文所说的“事务失效”包含两种不同现象：",[159,29282,29283,29286],{},[162,29284,29285],{},"事务根本没有创建，例如同类自调用绕过代理；",[162,29287,29288],{},"事务已经创建，但没有按照预期提交或回滚，例如异常被吞掉、切换线程或使用了独立事务。",[18,29290,29291],{},"排查时必须先区分是哪一种。",[14,29293,29295],{"id":29294},"二对象没有交给-spring-管理","二、对象没有交给 Spring 管理",[18,29297,29298],{},"下面的对象是手动创建的：",[144,29300,29302],{"className":197,"code":29301,"language":199,"meta":150,"style":150},"OrderService orderService = new OrderService();\norderService.createOrder();\n",[22,29303,29304,29318],{"__ignoreMap":150},[203,29305,29306,29309,29311,29313,29316],{"class":205,"line":206},[203,29307,29308],{"class":219},"OrderService orderService ",[203,29310,495],{"class":209},[203,29312,212],{"class":209},[203,29314,29315],{"class":215}," OrderService",[203,29317,257],{"class":219},[203,29319,29320,29323,29326],{"class":205,"line":251},[203,29321,29322],{"class":219},"orderService.",[203,29324,29325],{"class":215},"createOrder",[203,29327,257],{"class":219},[18,29329,13487,29330,29333,29334,29336],{},[22,29331,29332],{},"createOrder()"," 上有 ",[22,29335,29216],{},"，这个对象也不是 Spring 容器创建的 Bean，没有对应的事务代理，因此注解不会生效。",[144,29338,29340],{"className":197,"code":29339,"language":199,"meta":150,"style":150},"public class OrderService {\n\n    @Transactional\n    public void createOrder() {\n        \u002F\u002F 数据库操作\n    }\n}\n",[22,29341,29342,29352,29356,29363,29374,29379,29383],{"__ignoreMap":150},[203,29343,29344,29346,29348,29350],{"class":205,"line":206},[203,29345,441],{"class":209},[203,29347,1536],{"class":209},[203,29349,29315],{"class":215},[203,29351,248],{"class":219},[203,29353,29354],{"class":205,"line":251},[203,29355,757],{"emptyLinePlaceholder":756},[203,29357,29358,29360],{"class":205,"line":260},[203,29359,20042],{"class":219},[203,29361,29362],{"class":209},"Transactional\n",[203,29364,29365,29367,29369,29372],{"class":205,"line":278},[203,29366,1555],{"class":209},[203,29368,567],{"class":209},[203,29370,29371],{"class":215}," createOrder",[203,29373,18242],{"class":219},[203,29375,29376],{"class":205,"line":285},[203,29377,29378],{"class":281},"        \u002F\u002F 数据库操作\n",[203,29380,29381],{"class":205,"line":541},[203,29382,751],{"class":219},[203,29384,29385],{"class":205,"line":549},[203,29386,288],{"class":219},[18,29388,29389],{},"应当让类成为 Spring Bean，并通过容器注入后调用：",[144,29391,29393],{"className":197,"code":29392,"language":199,"meta":150,"style":150},"@Service\npublic class OrderService {\n\n    @Transactional\n    public void createOrder() {\n        \u002F\u002F 数据库操作\n    }\n}\n",[22,29394,29395,29401,29411,29415,29421,29431,29435,29439],{"__ignoreMap":150},[203,29396,29397,29399],{"class":205,"line":206},[203,29398,20011],{"class":219},[203,29400,26541],{"class":209},[203,29402,29403,29405,29407,29409],{"class":205,"line":251},[203,29404,441],{"class":209},[203,29406,1536],{"class":209},[203,29408,29315],{"class":215},[203,29410,248],{"class":219},[203,29412,29413],{"class":205,"line":260},[203,29414,757],{"emptyLinePlaceholder":756},[203,29416,29417,29419],{"class":205,"line":278},[203,29418,20042],{"class":219},[203,29420,29362],{"class":209},[203,29422,29423,29425,29427,29429],{"class":205,"line":285},[203,29424,1555],{"class":209},[203,29426,567],{"class":209},[203,29428,29371],{"class":215},[203,29430,18242],{"class":219},[203,29432,29433],{"class":205,"line":541},[203,29434,29378],{"class":281},[203,29436,29437],{"class":205,"line":549},[203,29438,751],{"class":219},[203,29440,29441],{"class":205,"line":787},[203,29442,288],{"class":219},[18,29444,29445],{},"同时还要确认应用已经启用事务基础设施。Spring Boot 通常会根据依赖和事务管理器自动配置；非 Boot 项目一般需要显式配置事务管理器并启用注解事务管理。",[14,29447,29449],{"id":29448},"三同一个类中自调用","三、同一个类中自调用",[18,29451,29452],{},"这是最常见的失效场景之一：",[144,29454,29456],{"className":197,"code":29455,"language":199,"meta":150,"style":150},"@Service\npublic class OrderService {\n\n    public void create() {\n        saveOrder();\n    }\n\n    @Transactional\n    public void saveOrder() {\n        \u002F\u002F 数据库操作\n    }\n}\n",[22,29457,29458,29464,29474,29478,29489,29496,29500,29504,29510,29521,29525,29529],{"__ignoreMap":150},[203,29459,29460,29462],{"class":205,"line":206},[203,29461,20011],{"class":219},[203,29463,26541],{"class":209},[203,29465,29466,29468,29470,29472],{"class":205,"line":251},[203,29467,441],{"class":209},[203,29469,1536],{"class":209},[203,29471,29315],{"class":215},[203,29473,248],{"class":219},[203,29475,29476],{"class":205,"line":260},[203,29477,757],{"emptyLinePlaceholder":756},[203,29479,29480,29482,29484,29487],{"class":205,"line":278},[203,29481,1555],{"class":209},[203,29483,567],{"class":209},[203,29485,29486],{"class":215}," create",[203,29488,18242],{"class":219},[203,29490,29491,29494],{"class":205,"line":285},[203,29492,29493],{"class":215},"        saveOrder",[203,29495,257],{"class":219},[203,29497,29498],{"class":205,"line":541},[203,29499,751],{"class":219},[203,29501,29502],{"class":205,"line":549},[203,29503,757],{"emptyLinePlaceholder":756},[203,29505,29506,29508],{"class":205,"line":787},[203,29507,20042],{"class":219},[203,29509,29362],{"class":209},[203,29511,29512,29514,29516,29519],{"class":205,"line":799},[203,29513,1555],{"class":209},[203,29515,567],{"class":209},[203,29517,29518],{"class":215}," saveOrder",[203,29520,18242],{"class":219},[203,29522,29523],{"class":205,"line":1371},[203,29524,29378],{"class":281},[203,29526,29527],{"class":205,"line":17966},[203,29528,751],{"class":219},[203,29530,29531],{"class":205,"line":17989},[203,29532,288],{"class":219},[18,29534,29535,29536,29539,29540,29543,29544,29547],{},"外部调用 ",[22,29537,29538],{},"create()"," 时，代理把调用转交给真实的 ",[22,29541,29542],{},"OrderService","。进入真实对象后，",[22,29545,29546],{},"saveOrder()"," 实际相当于：",[144,29549,29551],{"className":197,"code":29550,"language":199,"meta":150,"style":150},"this.saveOrder();\n",[22,29552,29553],{"__ignoreMap":150},[203,29554,29555,29557,29559,29562],{"class":205,"line":206},[203,29556,2286],{"class":718},[203,29558,9766],{"class":219},[203,29560,29561],{"class":215},"saveOrder",[203,29563,257],{"class":219},[18,29565,29566,29567,29569],{},"这次调用直接发生在目标对象内部，没有再次经过事务代理，所以 ",[22,29568,29546],{}," 上的事务拦截器不会执行。",[144,29571,29574],{"className":29572,"code":29573,"language":149,"meta":150},[147],"外部调用 → 代理 → 真实对象.create()\n                         ↓\n                   this.saveOrder()\n                         ↓\n                   绕过事务代理\n",[22,29575,29573],{"__ignoreMap":150},[18,29577,29578],{},"优先采用的解决方式是拆分职责，让调用跨 Bean 进行：",[144,29580,29582],{"className":197,"code":29581,"language":199,"meta":150,"style":150},"@Service\npublic class OrderService {\n\n    private final OrderWriter orderWriter;\n\n    public OrderService(OrderWriter orderWriter) {\n        this.orderWriter = orderWriter;\n    }\n\n    public void create() {\n        orderWriter.saveOrder();\n    }\n}\n\n@Service\npublic class OrderWriter {\n\n    @Transactional\n    public void saveOrder() {\n        \u002F\u002F 数据库操作\n    }\n}\n",[22,29583,29584,29590,29600,29604,29613,29617,29631,29643,29647,29651,29661,29670,29674,29678,29682,29688,29699,29703,29709,29719,29723,29727],{"__ignoreMap":150},[203,29585,29586,29588],{"class":205,"line":206},[203,29587,20011],{"class":219},[203,29589,26541],{"class":209},[203,29591,29592,29594,29596,29598],{"class":205,"line":251},[203,29593,441],{"class":209},[203,29595,1536],{"class":209},[203,29597,29315],{"class":215},[203,29599,248],{"class":219},[203,29601,29602],{"class":205,"line":260},[203,29603,757],{"emptyLinePlaceholder":756},[203,29605,29606,29608,29610],{"class":205,"line":278},[203,29607,17969],{"class":209},[203,29609,17952],{"class":209},[203,29611,29612],{"class":219}," OrderWriter orderWriter;\n",[203,29614,29615],{"class":205,"line":285},[203,29616,757],{"emptyLinePlaceholder":756},[203,29618,29619,29621,29623,29626,29629],{"class":205,"line":541},[203,29620,1555],{"class":209},[203,29622,29315],{"class":215},[203,29624,29625],{"class":219},"(OrderWriter ",[203,29627,29628],{"class":271},"orderWriter",[203,29630,275],{"class":219},[203,29632,29633,29635,29638,29640],{"class":205,"line":549},[203,29634,25942],{"class":718},[203,29636,29637],{"class":219},".orderWriter ",[203,29639,495],{"class":209},[203,29641,29642],{"class":219}," orderWriter;\n",[203,29644,29645],{"class":205,"line":787},[203,29646,751],{"class":219},[203,29648,29649],{"class":205,"line":799},[203,29650,757],{"emptyLinePlaceholder":756},[203,29652,29653,29655,29657,29659],{"class":205,"line":1371},[203,29654,1555],{"class":209},[203,29656,567],{"class":209},[203,29658,29486],{"class":215},[203,29660,18242],{"class":219},[203,29662,29663,29666,29668],{"class":205,"line":17966},[203,29664,29665],{"class":219},"        orderWriter.",[203,29667,29561],{"class":215},[203,29669,257],{"class":219},[203,29671,29672],{"class":205,"line":17989},[203,29673,751],{"class":219},[203,29675,29676],{"class":205,"line":17994},[203,29677,288],{"class":219},[203,29679,29680],{"class":205,"line":18015},[203,29681,757],{"emptyLinePlaceholder":756},[203,29683,29684,29686],{"class":205,"line":18030},[203,29685,20011],{"class":219},[203,29687,26541],{"class":209},[203,29689,29690,29692,29694,29697],{"class":205,"line":18036},[203,29691,441],{"class":209},[203,29693,1536],{"class":209},[203,29695,29696],{"class":215}," OrderWriter",[203,29698,248],{"class":219},[203,29700,29701],{"class":205,"line":18053},[203,29702,757],{"emptyLinePlaceholder":756},[203,29704,29705,29707],{"class":205,"line":18059},[203,29706,20042],{"class":219},[203,29708,29362],{"class":209},[203,29710,29711,29713,29715,29717],{"class":205,"line":18073},[203,29712,1555],{"class":209},[203,29714,567],{"class":209},[203,29716,29518],{"class":215},[203,29718,18242],{"class":219},[203,29720,29721],{"class":205,"line":18083},[203,29722,29378],{"class":281},[203,29724,29725],{"class":205,"line":18109},[203,29726,751],{"class":219},[203,29728,29729],{"class":205,"line":18115},[203,29730,288],{"class":219},[18,29732,29733,29734,29737],{},"也可以使用 ",[22,29735,29736],{},"TransactionTemplate"," 显式划分事务边界。",[18,29739,29740],{},"如果暂时不方便拆分类，也可以让 Spring 注入当前 Bean 的代理引用，再通过该引用调用事务方法：",[144,29742,29744],{"className":197,"code":29743,"language":199,"meta":150,"style":150},"@Service\npublic class OrderService {\n\n    private OrderService self;\n\n    @Autowired\n    public void setSelf(@Lazy OrderService self) {\n        this.self = self;\n    }\n\n    public void create() {\n        self.saveOrder();\n    }\n\n    @Transactional\n    public void saveOrder() {\n        \u002F\u002F 数据库操作\n    }\n}\n",[22,29745,29746,29752,29762,29766,29773,29777,29784,29807,29819,29823,29827,29837,29846,29850,29854,29860,29870,29874,29878],{"__ignoreMap":150},[203,29747,29748,29750],{"class":205,"line":206},[203,29749,20011],{"class":219},[203,29751,26541],{"class":209},[203,29753,29754,29756,29758,29760],{"class":205,"line":251},[203,29755,441],{"class":209},[203,29757,1536],{"class":209},[203,29759,29315],{"class":215},[203,29761,248],{"class":219},[203,29763,29764],{"class":205,"line":260},[203,29765,757],{"emptyLinePlaceholder":756},[203,29767,29768,29770],{"class":205,"line":278},[203,29769,17969],{"class":209},[203,29771,29772],{"class":219}," OrderService self;\n",[203,29774,29775],{"class":205,"line":285},[203,29776,757],{"emptyLinePlaceholder":756},[203,29778,29779,29781],{"class":205,"line":541},[203,29780,20042],{"class":219},[203,29782,29783],{"class":209},"Autowired\n",[203,29785,29786,29788,29790,29793,29796,29799,29802,29805],{"class":205,"line":549},[203,29787,1555],{"class":209},[203,29789,567],{"class":209},[203,29791,29792],{"class":215}," setSelf",[203,29794,29795],{"class":219},"(@",[203,29797,29798],{"class":209},"Lazy",[203,29800,29801],{"class":219}," OrderService ",[203,29803,29804],{"class":271},"self",[203,29806,275],{"class":219},[203,29808,29809,29811,29814,29816],{"class":205,"line":787},[203,29810,25942],{"class":718},[203,29812,29813],{"class":219},".self ",[203,29815,495],{"class":209},[203,29817,29818],{"class":219}," self;\n",[203,29820,29821],{"class":205,"line":799},[203,29822,751],{"class":219},[203,29824,29825],{"class":205,"line":1371},[203,29826,757],{"emptyLinePlaceholder":756},[203,29828,29829,29831,29833,29835],{"class":205,"line":17966},[203,29830,1555],{"class":209},[203,29832,567],{"class":209},[203,29834,29486],{"class":215},[203,29836,18242],{"class":219},[203,29838,29839,29842,29844],{"class":205,"line":17989},[203,29840,29841],{"class":219},"        self.",[203,29843,29561],{"class":215},[203,29845,257],{"class":219},[203,29847,29848],{"class":205,"line":17994},[203,29849,751],{"class":219},[203,29851,29852],{"class":205,"line":18015},[203,29853,757],{"emptyLinePlaceholder":756},[203,29855,29856,29858],{"class":205,"line":18030},[203,29857,20042],{"class":219},[203,29859,29362],{"class":209},[203,29861,29862,29864,29866,29868],{"class":205,"line":18036},[203,29863,1555],{"class":209},[203,29865,567],{"class":209},[203,29867,29518],{"class":215},[203,29869,18242],{"class":219},[203,29871,29872],{"class":205,"line":18053},[203,29873,29378],{"class":281},[203,29875,29876],{"class":205,"line":18059},[203,29877,751],{"class":219},[203,29879,29880],{"class":205,"line":18073},[203,29881,288],{"class":219},[18,29883,8521,29884,29886,29887,29889,29890,29893,29894,29896,29897,29900],{},[22,29885,29804],{}," 不是简单的 ",[22,29888,2286],{},"，而是由 Spring 容器提供的代理引用。",[22,29891,29892],{},"@Lazy"," 让这个依赖延迟解析，避免在创建 ",[22,29895,29542],{}," 时立即要求再次完整创建同一个 Bean。调用 ",[22,29898,29899],{},"self.saveOrder()"," 会先经过事务代理，因此事务拦截器能够生效。",[18,29902,29903],{},"这种写法虽然可以解决同类调用问题，但会让类依赖自己的代理，也形成了自引用结构。职责能够拆分时，仍然优先使用前面的跨 Bean 调用方案。",[18,29905,29906,29907,431],{},"另一种方式是开启 ",[22,29908,29909],{},"exposeProxy",[144,29911,29913],{"className":197,"code":29912,"language":199,"meta":150,"style":150},"@Configuration\n@EnableAspectJAutoProxy(exposeProxy = true)\npublic class AopConfig {\n}\n",[22,29914,29915,29922,29939,29950],{"__ignoreMap":150},[203,29916,29917,29919],{"class":205,"line":206},[203,29918,20011],{"class":219},[203,29920,29921],{"class":209},"Configuration\n",[203,29923,29924,29926,29929,29931,29933,29935,29937],{"class":205,"line":251},[203,29925,20011],{"class":219},[203,29927,29928],{"class":209},"EnableAspectJAutoProxy",[203,29930,220],{"class":219},[203,29932,29909],{"class":718},[203,29934,9514],{"class":209},[203,29936,21563],{"class":718},[203,29938,12343],{"class":219},[203,29940,29941,29943,29945,29948],{"class":205,"line":260},[203,29942,441],{"class":209},[203,29944,1536],{"class":209},[203,29946,29947],{"class":215}," AopConfig",[203,29949,248],{"class":219},[203,29951,29952],{"class":205,"line":278},[203,29953,288],{"class":219},[18,29955,29956,29958,29959,29962],{},[22,29957,29909],{}," 表示 Spring 在执行一次 AOP 代理调用时，把当前正在工作的代理对象临时保存在线程上下文中，使目标对象可以通过 ",[22,29960,29961],{},"AopContext.currentProxy()"," 取得它：",[144,29964,29966],{"className":197,"code":29965,"language":199,"meta":150,"style":150},"@Service\npublic class OrderService {\n\n    public void create() {\n        OrderService proxy =\n                (OrderService) AopContext.currentProxy();\n\n        proxy.saveOrder();\n    }\n\n    @Transactional\n    public void saveOrder() {\n        \u002F\u002F 数据库操作\n    }\n}\n",[22,29967,29968,29974,29984,29988,29998,30005,30015,30019,30028,30032,30036,30042,30052,30056,30060],{"__ignoreMap":150},[203,29969,29970,29972],{"class":205,"line":206},[203,29971,20011],{"class":219},[203,29973,26541],{"class":209},[203,29975,29976,29978,29980,29982],{"class":205,"line":251},[203,29977,441],{"class":209},[203,29979,1536],{"class":209},[203,29981,29315],{"class":215},[203,29983,248],{"class":219},[203,29985,29986],{"class":205,"line":260},[203,29987,757],{"emptyLinePlaceholder":756},[203,29989,29990,29992,29994,29996],{"class":205,"line":278},[203,29991,1555],{"class":209},[203,29993,567],{"class":209},[203,29995,29486],{"class":215},[203,29997,18242],{"class":219},[203,29999,30000,30003],{"class":205,"line":285},[203,30001,30002],{"class":219},"        OrderService proxy ",[203,30004,16382],{"class":209},[203,30006,30007,30010,30013],{"class":205,"line":541},[203,30008,30009],{"class":219},"                (OrderService) AopContext.",[203,30011,30012],{"class":215},"currentProxy",[203,30014,257],{"class":219},[203,30016,30017],{"class":205,"line":549},[203,30018,757],{"emptyLinePlaceholder":756},[203,30020,30021,30024,30026],{"class":205,"line":787},[203,30022,30023],{"class":219},"        proxy.",[203,30025,29561],{"class":215},[203,30027,257],{"class":219},[203,30029,30030],{"class":205,"line":799},[203,30031,751],{"class":219},[203,30033,30034],{"class":205,"line":1371},[203,30035,757],{"emptyLinePlaceholder":756},[203,30037,30038,30040],{"class":205,"line":17966},[203,30039,20042],{"class":219},[203,30041,29362],{"class":209},[203,30043,30044,30046,30048,30050],{"class":205,"line":17989},[203,30045,1555],{"class":209},[203,30047,567],{"class":209},[203,30049,29518],{"class":215},[203,30051,18242],{"class":219},[203,30053,30054],{"class":205,"line":17994},[203,30055,29378],{"class":281},[203,30057,30058],{"class":205,"line":18015},[203,30059,751],{"class":219},[203,30061,30062],{"class":205,"line":18030},[203,30063,288],{"class":219},[18,30065,30066,30069,30070,30073,30074,30077,30078,30080,30081,33],{},[22,30067,30068],{},"exposeProxy = true"," 不会让 ",[22,30071,30072],{},"this.saveOrder()"," 自动经过代理，仍然必须显式使用当前代理调用。并且，调用 ",[22,30075,30076],{},"currentProxy()"," 的方法本身必须是从 AOP 代理入口进入的；如果当前线程不在一次 AOP 代理调用中，",[22,30079,30076],{}," 会抛出 ",[22,30082,841],{},[18,30084,30085],{},"这种方案让业务类直接依赖 Spring AOP，并带来额外的代理上下文维护成本，通常只作为最后手段。",[14,30087,30089],{"id":30088},"四方法无法被代理增强","四、方法无法被代理增强",[18,30091,30092],{},"在常见的 Spring AOP 代理模式下，以下方法不能作为可靠的事务入口：",[473,30094,30096],{"id":30095},"_1-private-方法","1. private 方法",[144,30098,30100],{"className":197,"code":30099,"language":199,"meta":150,"style":150},"@Transactional\nprivate void saveOrder() {\n}\n",[22,30101,30102,30108,30118],{"__ignoreMap":150},[203,30103,30104,30106],{"class":205,"line":206},[203,30105,20011],{"class":219},[203,30107,29362],{"class":209},[203,30109,30110,30112,30114,30116],{"class":205,"line":251},[203,30111,25991],{"class":209},[203,30113,567],{"class":209},[203,30115,29518],{"class":215},[203,30117,18242],{"class":219},[203,30119,30120],{"class":205,"line":260},[203,30121,288],{"class":219},[18,30123,30124],{},"私有方法不能被子类覆盖，也不会作为接口代理的外部调用入口，因此不能依赖它建立声明式事务。",[473,30126,30128],{"id":30127},"_2-final-方法","2. final 方法",[144,30130,30132],{"className":197,"code":30131,"language":199,"meta":150,"style":150},"@Transactional\npublic final void saveOrder() {\n}\n",[22,30133,30134,30140,30152],{"__ignoreMap":150},[203,30135,30136,30138],{"class":205,"line":206},[203,30137,20011],{"class":219},[203,30139,29362],{"class":209},[203,30141,30142,30144,30146,30148,30150],{"class":205,"line":251},[203,30143,441],{"class":209},[203,30145,17952],{"class":209},[203,30147,567],{"class":209},[203,30149,29518],{"class":215},[203,30151,18242],{"class":219},[203,30153,30154],{"class":205,"line":260},[203,30155,288],{"class":219},[18,30157,30158,30159,30161],{},"CGLIB 类代理依靠生成子类并覆盖方法完成增强，",[22,30160,25884],{}," 方法不能被覆盖，所以无法被这种方式增强。",[473,30163,30165],{"id":30164},"_3-static-方法","3. static 方法",[144,30167,30169],{"className":197,"code":30168,"language":199,"meta":150,"style":150},"@Transactional\npublic static void saveOrder() {\n}\n",[22,30170,30171,30177,30189],{"__ignoreMap":150},[203,30172,30173,30175],{"class":205,"line":206},[203,30174,20011],{"class":219},[203,30176,29362],{"class":209},[203,30178,30179,30181,30183,30185,30187],{"class":205,"line":251},[203,30180,441],{"class":209},[203,30182,17972],{"class":209},[203,30184,567],{"class":209},[203,30186,29518],{"class":215},[203,30188,18242],{"class":219},[203,30190,30191],{"class":205,"line":260},[203,30192,288],{"class":219},[18,30194,30195],{},"静态方法属于类，不属于 Spring 管理的 Bean 实例，实例代理无法拦截这种调用。",[473,30197,30199],{"id":30198},"_4-非-public-方法","4. 非 public 方法",[18,30201,30202,30203,30206,30207,30209],{},"Spring Framework 6 以后，基于类的代理可以支持 ",[22,30204,30205],{},"protected"," 和包可见方法，但接口代理仍然主要面向接口中的 ",[22,30208,441],{}," 方法，不同代理方式和版本存在差异。对入门级业务代码而言，最稳妥的规则仍然是：",[105,30211,30212],{},[18,30213,30214],{},"把事务边界放在 Spring Bean 的可覆盖 public 方法上，并从其他 Bean 通过代理调用。",[14,30216,30218],{"id":30217},"五在-bean-初始化阶段调用事务方法","五、在 Bean 初始化阶段调用事务方法",[18,30220,30221,30222,30224],{},"不要依赖 ",[22,30223,19253],{}," 中的事务调用：",[144,30226,30228],{"className":197,"code":30227,"language":199,"meta":150,"style":150},"@PostConstruct\npublic void init() {\n    loadData();\n}\n\n@Transactional\npublic void loadData() {\n    \u002F\u002F 数据库操作\n}\n",[22,30229,30230,30236,30246,30252,30256,30260,30266,30277,30282],{"__ignoreMap":150},[203,30231,30232,30234],{"class":205,"line":206},[203,30233,20011],{"class":219},[203,30235,20804],{"class":209},[203,30237,30238,30240,30242,30244],{"class":205,"line":251},[203,30239,441],{"class":209},[203,30241,567],{"class":209},[203,30243,20867],{"class":215},[203,30245,18242],{"class":219},[203,30247,30248,30250],{"class":205,"line":260},[203,30249,1238],{"class":215},[203,30251,257],{"class":219},[203,30253,30254],{"class":205,"line":278},[203,30255,288],{"class":219},[203,30257,30258],{"class":205,"line":285},[203,30259,757],{"emptyLinePlaceholder":756},[203,30261,30262,30264],{"class":205,"line":541},[203,30263,20011],{"class":219},[203,30265,29362],{"class":209},[203,30267,30268,30270,30272,30275],{"class":205,"line":549},[203,30269,441],{"class":209},[203,30271,567],{"class":209},[203,30273,30274],{"class":215}," loadData",[203,30276,18242],{"class":219},[203,30278,30279],{"class":205,"line":787},[203,30280,30281],{"class":281},"    \u002F\u002F 数据库操作\n",[203,30283,30284],{"class":205,"line":799},[203,30285,288],{"class":219},[18,30287,30288,30290,30291,30294,30295,30297,30298,30300],{},[22,30289,19253],{}," 本身属于 Bean 的初始化回调，但 Spring 是通过 ",[22,30292,30293],{},"InitDestroyAnnotationBeanPostProcessor"," 在 ",[22,30296,19922],{}," 阶段调用它的。这里的“BeforeInitialization”是 Bean 后置处理器的回调名称，并不是说 ",[22,30299,19253],{}," 发生在整个 Bean 初始化过程开始之前。",[18,30302,30303],{},"相关顺序可以简化为：",[144,30305,30308],{"className":30306,"code":30307,"language":149,"meta":150},[147],"实例化 Bean\n  ↓\n属性填充与依赖注入\n  ↓\nAware 回调\n  ↓\nBeanPostProcessor.postProcessBeforeInitialization()\n  └─ 调用 @PostConstruct\n  ↓\nInitializingBean.afterPropertiesSet()\n  ↓\n自定义 init-method\n  ↓\nBeanPostProcessor.postProcessAfterInitialization()\n  └─ 通常在这里创建 AOP 代理\n",[22,30309,30307],{"__ignoreMap":150},[18,30311,30312,30313,30315,30316,30319,30320,30323],{},"因此，执行 ",[22,30314,19253],{}," 时通常仍然是在原始目标 Bean 上运行初始化回调，事务拦截器尚未应用到这个回调。示例中的 ",[22,30317,30318],{},"loadData()"," 又是一次 ",[22,30321,30322],{},"this.loadData()"," 同类调用，也不会经过事务代理。",[18,30325,30326,30327,30330],{},"在 Spring Boot 应用中，可以等容器启动完成后，由一个 Bean 监听 ",[22,30328,30329],{},"ApplicationReadyEvent","，再调用另一个 Bean 的事务方法：",[144,30332,30334],{"className":197,"code":30333,"language":199,"meta":150,"style":150},"@Component\npublic class StartupInitializer {\n\n    private final DataLoader dataLoader;\n\n    public StartupInitializer(DataLoader dataLoader) {\n        this.dataLoader = dataLoader;\n    }\n\n    @EventListener(ApplicationReadyEvent.class)\n    public void initialize() {\n        dataLoader.loadData();\n    }\n}\n\n@Service\npublic class DataLoader {\n\n    @Transactional\n    public void loadData() {\n        \u002F\u002F 数据库初始化操作\n    }\n}\n",[22,30335,30336,30342,30353,30357,30366,30370,30384,30396,30400,30404,30413,30423,30433,30437,30441,30445,30451,30462,30466,30472,30482,30487,30491],{"__ignoreMap":150},[203,30337,30338,30340],{"class":205,"line":206},[203,30339,20011],{"class":219},[203,30341,20014],{"class":209},[203,30343,30344,30346,30348,30351],{"class":205,"line":251},[203,30345,441],{"class":209},[203,30347,1536],{"class":209},[203,30349,30350],{"class":215}," StartupInitializer",[203,30352,248],{"class":219},[203,30354,30355],{"class":205,"line":260},[203,30356,757],{"emptyLinePlaceholder":756},[203,30358,30359,30361,30363],{"class":205,"line":278},[203,30360,17969],{"class":209},[203,30362,17952],{"class":209},[203,30364,30365],{"class":219}," DataLoader dataLoader;\n",[203,30367,30368],{"class":205,"line":285},[203,30369,757],{"emptyLinePlaceholder":756},[203,30371,30372,30374,30376,30379,30382],{"class":205,"line":541},[203,30373,1555],{"class":209},[203,30375,30350],{"class":215},[203,30377,30378],{"class":219},"(DataLoader ",[203,30380,30381],{"class":271},"dataLoader",[203,30383,275],{"class":219},[203,30385,30386,30388,30391,30393],{"class":205,"line":549},[203,30387,25942],{"class":718},[203,30389,30390],{"class":219},".dataLoader ",[203,30392,495],{"class":209},[203,30394,30395],{"class":219}," dataLoader;\n",[203,30397,30398],{"class":205,"line":787},[203,30399,751],{"class":219},[203,30401,30402],{"class":205,"line":799},[203,30403,757],{"emptyLinePlaceholder":756},[203,30405,30406,30408,30410],{"class":205,"line":1371},[203,30407,20042],{"class":219},[203,30409,21120],{"class":209},[203,30411,30412],{"class":219},"(ApplicationReadyEvent.class)\n",[203,30414,30415,30417,30419,30421],{"class":205,"line":17966},[203,30416,1555],{"class":209},[203,30418,567],{"class":209},[203,30420,570],{"class":215},[203,30422,18242],{"class":219},[203,30424,30425,30428,30431],{"class":205,"line":17989},[203,30426,30427],{"class":219},"        dataLoader.",[203,30429,30430],{"class":215},"loadData",[203,30432,257],{"class":219},[203,30434,30435],{"class":205,"line":17994},[203,30436,751],{"class":219},[203,30438,30439],{"class":205,"line":18015},[203,30440,288],{"class":219},[203,30442,30443],{"class":205,"line":18030},[203,30444,757],{"emptyLinePlaceholder":756},[203,30446,30447,30449],{"class":205,"line":18036},[203,30448,20011],{"class":219},[203,30450,26541],{"class":209},[203,30452,30453,30455,30457,30460],{"class":205,"line":18053},[203,30454,441],{"class":209},[203,30456,1536],{"class":209},[203,30458,30459],{"class":215}," DataLoader",[203,30461,248],{"class":219},[203,30463,30464],{"class":205,"line":18059},[203,30465,757],{"emptyLinePlaceholder":756},[203,30467,30468,30470],{"class":205,"line":18073},[203,30469,20042],{"class":219},[203,30471,29362],{"class":209},[203,30473,30474,30476,30478,30480],{"class":205,"line":18083},[203,30475,1555],{"class":209},[203,30477,567],{"class":209},[203,30479,30274],{"class":215},[203,30481,18242],{"class":219},[203,30483,30484],{"class":205,"line":18109},[203,30485,30486],{"class":281},"        \u002F\u002F 数据库初始化操作\n",[203,30488,30489],{"class":205,"line":18115},[203,30490,751],{"class":219},[203,30492,30493],{"class":205,"line":18120},[203,30494,288],{"class":219},[18,30496,30497,30498,30501,30502,30505],{},"此时容器已经完成 Bean 创建，",[22,30499,30500],{},"StartupInitializer"," 调用的是 Spring 注入的 ",[22,30503,30504],{},"DataLoader"," 代理，事务拦截器可以正常建立事务边界。",[14,30507,30509],{"id":30508},"六异常没有命中回滚规则","六、异常没有命中回滚规则",[18,30511,30512],{},"Spring 默认对以下异常回滚：",[159,30514,30515,30520],{},[162,30516,30517,30519],{},[22,30518,69],{}," 及其子类；",[162,30521,30522,30524],{},[22,30523,39],{}," 及其子类。",[18,30526,30527],{},"默认不会因为受检异常自动回滚：",[144,30529,30531],{"className":197,"code":30530,"language":199,"meta":150,"style":150},"@Transactional\npublic void importFile(Path source, Path archive) throws IOException {\n    List\u003CString> lines = Files.readAllLines(\n            source,\n            StandardCharsets.UTF_8\n    );\n\n    for (String line : lines) {\n        ImportRecord record = parse(line);\n        repository.save(record);\n    }\n\n    Files.move(\n            source,\n            archive,\n            StandardCopyOption.REPLACE_EXISTING\n    );\n}\n",[22,30532,30533,30539,30551,30570,30575,30580,30584,30588,30600,30613,30623,30627,30631,30641,30645,30650,30655,30659],{"__ignoreMap":150},[203,30534,30535,30537],{"class":205,"line":206},[203,30536,20011],{"class":219},[203,30538,29362],{"class":209},[203,30540,30541,30543,30545,30548],{"class":205,"line":251},[203,30542,441],{"class":209},[203,30544,567],{"class":209},[203,30546,30547],{"class":215}," importFile",[203,30549,30550],{"class":219},"(Path source, Path archive) throws IOException {\n",[203,30552,30553,30556,30558,30561,30563,30565,30568],{"class":205,"line":260},[203,30554,30555],{"class":219},"    List\u003C",[203,30557,5176],{"class":209},[203,30559,30560],{"class":219},"> lines ",[203,30562,495],{"class":209},[203,30564,458],{"class":219},[203,30566,30567],{"class":215},"readAllLines",[203,30569,1496],{"class":219},[203,30571,30572],{"class":205,"line":278},[203,30573,30574],{"class":219},"            source,\n",[203,30576,30577],{"class":205,"line":285},[203,30578,30579],{"class":219},"            StandardCharsets.UTF_8\n",[203,30581,30582],{"class":205,"line":541},[203,30583,1517],{"class":219},[203,30585,30586],{"class":205,"line":549},[203,30587,757],{"emptyLinePlaceholder":756},[203,30589,30590,30592,30595,30597],{"class":205,"line":787},[203,30591,5214],{"class":209},[203,30593,30594],{"class":219}," (String line ",[203,30596,5229],{"class":209},[203,30598,30599],{"class":219}," lines) {\n",[203,30601,30602,30605,30607,30610],{"class":205,"line":799},[203,30603,30604],{"class":219},"        ImportRecord record ",[203,30606,495],{"class":209},[203,30608,30609],{"class":215}," parse",[203,30611,30612],{"class":219},"(line);\n",[203,30614,30615,30618,30621],{"class":205,"line":1371},[203,30616,30617],{"class":219},"        repository.",[203,30619,30620],{"class":215},"save",[203,30622,4867],{"class":219},[203,30624,30625],{"class":205,"line":17966},[203,30626,751],{"class":219},[203,30628,30629],{"class":205,"line":17989},[203,30630,757],{"emptyLinePlaceholder":756},[203,30632,30633,30636,30639],{"class":205,"line":17994},[203,30634,30635],{"class":219},"    Files.",[203,30637,30638],{"class":215},"move",[203,30640,1496],{"class":219},[203,30642,30643],{"class":205,"line":18015},[203,30644,30574],{"class":219},[203,30646,30647],{"class":205,"line":18030},[203,30648,30649],{"class":219},"            archive,\n",[203,30651,30652],{"class":205,"line":18036},[203,30653,30654],{"class":219},"            StandardCopyOption.REPLACE_EXISTING\n",[203,30656,30657],{"class":205,"line":18053},[203,30658,1517],{"class":219},[203,30660,30661],{"class":205,"line":18059},[203,30662,288],{"class":219},[18,30664,30665,30666,30669,30670,30672,30673,30675],{},"假设文件已经读取完成、数据库记录也已经写入，但归档文件时 ",[22,30667,30668],{},"Files.move()"," 抛出 ",[22,30671,76],{},"。因为 ",[22,30674,76],{}," 是受检异常，Spring 默认仍会提交数据库事务，而不是自动回滚前面写入的记录。",[18,30677,30678,30679,30681],{},"如果业务要求 ",[22,30680,76],{}," 也触发回滚，需要明确配置：",[144,30683,30685],{"className":197,"code":30684,"language":199,"meta":150,"style":150},"@Transactional(rollbackFor = IOException.class)\npublic void importFile(Path source, Path archive) throws IOException {\n    List\u003CString> lines = Files.readAllLines(\n            source,\n            StandardCharsets.UTF_8\n    );\n\n    for (String line : lines) {\n        ImportRecord record = parse(line);\n        repository.save(record);\n    }\n\n    Files.move(\n            source,\n            archive,\n            StandardCopyOption.REPLACE_EXISTING\n    );\n}\n",[22,30686,30687,30704,30714,30730,30734,30738,30742,30746,30756,30766,30774,30778,30782,30790,30794,30798,30802,30806],{"__ignoreMap":150},[203,30688,30689,30691,30694,30696,30699,30701],{"class":205,"line":206},[203,30690,20011],{"class":219},[203,30692,30693],{"class":209},"Transactional",[203,30695,220],{"class":219},[203,30697,30698],{"class":718},"rollbackFor",[203,30700,9514],{"class":209},[203,30702,30703],{"class":219}," IOException.class)\n",[203,30705,30706,30708,30710,30712],{"class":205,"line":251},[203,30707,441],{"class":209},[203,30709,567],{"class":209},[203,30711,30547],{"class":215},[203,30713,30550],{"class":219},[203,30715,30716,30718,30720,30722,30724,30726,30728],{"class":205,"line":260},[203,30717,30555],{"class":219},[203,30719,5176],{"class":209},[203,30721,30560],{"class":219},[203,30723,495],{"class":209},[203,30725,458],{"class":219},[203,30727,30567],{"class":215},[203,30729,1496],{"class":219},[203,30731,30732],{"class":205,"line":278},[203,30733,30574],{"class":219},[203,30735,30736],{"class":205,"line":285},[203,30737,30579],{"class":219},[203,30739,30740],{"class":205,"line":541},[203,30741,1517],{"class":219},[203,30743,30744],{"class":205,"line":549},[203,30745,757],{"emptyLinePlaceholder":756},[203,30747,30748,30750,30752,30754],{"class":205,"line":787},[203,30749,5214],{"class":209},[203,30751,30594],{"class":219},[203,30753,5229],{"class":209},[203,30755,30599],{"class":219},[203,30757,30758,30760,30762,30764],{"class":205,"line":799},[203,30759,30604],{"class":219},[203,30761,495],{"class":209},[203,30763,30609],{"class":215},[203,30765,30612],{"class":219},[203,30767,30768,30770,30772],{"class":205,"line":1371},[203,30769,30617],{"class":219},[203,30771,30620],{"class":215},[203,30773,4867],{"class":219},[203,30775,30776],{"class":205,"line":17966},[203,30777,751],{"class":219},[203,30779,30780],{"class":205,"line":17989},[203,30781,757],{"emptyLinePlaceholder":756},[203,30783,30784,30786,30788],{"class":205,"line":17994},[203,30785,30635],{"class":219},[203,30787,30638],{"class":215},[203,30789,1496],{"class":219},[203,30791,30792],{"class":205,"line":18015},[203,30793,30574],{"class":219},[203,30795,30796],{"class":205,"line":18030},[203,30797,30649],{"class":219},[203,30799,30800],{"class":205,"line":18036},[203,30801,30654],{"class":219},[203,30803,30804],{"class":205,"line":18053},[203,30805,1517],{"class":219},[203,30807,30808],{"class":205,"line":18059},[203,30809,288],{"class":219},[18,30811,30812],{},"这里的事务只能回滚数据库操作，不能撤销已经成功完成的文件系统操作。涉及数据库与文件、消息等多种资源时，还需要结合操作顺序、补偿或可靠事件方案处理跨资源一致性。",[18,30814,30815,30818,30819,53,30821,30823],{},[22,30816,30817],{},"noRollbackFor"," 则表示某类异常不触发回滚。排查时要同时检查 ",[22,30820,30698],{},[22,30822,30817],{}," 以及异常的实际类型，不能只看方法是否抛出了异常。",[14,30825,30827],{"id":30826},"七异常被捕获后没有继续抛出","七、异常被捕获后没有继续抛出",[18,30829,30830],{},"事务拦截器需要观察目标方法的返回或异常，才能决定提交还是回滚：",[144,30832,30834],{"className":197,"code":30833,"language":199,"meta":150,"style":150},"@Transactional\npublic void createOrder() {\n    try {\n        orderRepository.save(order);\n        inventoryRepository.deduct(stock);\n    } catch (RuntimeException exception) {\n        log.error(\"创建订单失败\", exception);\n    }\n}\n",[22,30835,30836,30842,30852,30859,30869,30880,30893,30907,30911],{"__ignoreMap":150},[203,30837,30838,30840],{"class":205,"line":206},[203,30839,20011],{"class":219},[203,30841,29362],{"class":209},[203,30843,30844,30846,30848,30850],{"class":205,"line":251},[203,30845,441],{"class":209},[203,30847,567],{"class":209},[203,30849,29371],{"class":215},[203,30851,18242],{"class":219},[203,30853,30854,30857],{"class":205,"line":260},[203,30855,30856],{"class":209},"    try",[203,30858,248],{"class":219},[203,30860,30861,30864,30866],{"class":205,"line":278},[203,30862,30863],{"class":219},"        orderRepository.",[203,30865,30620],{"class":215},[203,30867,30868],{"class":219},"(order);\n",[203,30870,30871,30874,30877],{"class":205,"line":285},[203,30872,30873],{"class":219},"        inventoryRepository.",[203,30875,30876],{"class":215},"deduct",[203,30878,30879],{"class":219},"(stock);\n",[203,30881,30882,30885,30887,30889,30891],{"class":205,"line":541},[203,30883,30884],{"class":219},"    } ",[203,30886,28],{"class":209},[203,30888,1146],{"class":219},[203,30890,272],{"class":271},[203,30892,275],{"class":219},[203,30894,30895,30898,30900,30902,30905],{"class":205,"line":549},[203,30896,30897],{"class":219},"        log.",[203,30899,530],{"class":215},[203,30901,220],{"class":219},[203,30903,30904],{"class":223},"\"创建订单失败\"",[203,30906,538],{"class":219},[203,30908,30909],{"class":205,"line":787},[203,30910,751],{"class":219},[203,30912,30913],{"class":205,"line":799},[203,30914,288],{"class":219},[18,30916,30917],{},"异常在方法内部被捕获后，方法对事务拦截器表现为正常返回，默认结果就是提交。",[18,30919,30920],{},"通常应让异常继续向外抛出：",[144,30922,30924],{"className":197,"code":30923,"language":199,"meta":150,"style":150},"catch (RuntimeException exception) {\n    log.error(\"创建订单失败\", exception);\n    throw exception;\n}\n",[22,30925,30926,30936,30948,30954],{"__ignoreMap":150},[203,30927,30928,30930,30932,30934],{"class":205,"line":206},[203,30929,28],{"class":209},[203,30931,1146],{"class":219},[203,30933,272],{"class":271},[203,30935,275],{"class":219},[203,30937,30938,30940,30942,30944,30946],{"class":205,"line":251},[203,30939,527],{"class":219},[203,30941,530],{"class":215},[203,30943,220],{"class":219},[203,30945,30904],{"class":223},[203,30947,538],{"class":219},[203,30949,30950,30952],{"class":205,"line":260},[203,30951,1488],{"class":209},[203,30953,18320],{"class":219},[203,30955,30956],{"class":205,"line":278},[203,30957,288],{"class":219},[18,30959,30960],{},"如果业务必须吞掉异常，也可以显式标记当前事务只能回滚：",[144,30962,30964],{"className":197,"code":30963,"language":199,"meta":150,"style":150},"TransactionAspectSupport\n        .currentTransactionStatus()\n        .setRollbackOnly();\n",[22,30965,30966,30971,30981],{"__ignoreMap":150},[203,30967,30968],{"class":205,"line":206},[203,30969,30970],{"class":219},"TransactionAspectSupport\n",[203,30972,30973,30976,30979],{"class":205,"line":251},[203,30974,30975],{"class":219},"        .",[203,30977,30978],{"class":215},"currentTransactionStatus",[203,30980,18050],{"class":219},[203,30982,30983,30985,30988],{"class":205,"line":260},[203,30984,30975],{"class":219},[203,30986,30987],{"class":215},"setRollbackOnly",[203,30989,257],{"class":219},[18,30991,30992],{},"不过这种方式让业务代码依赖 Spring 事务 API，能够通过异常和声明式规则表达时，优先使用声明式方式。",[14,30994,30996],{"id":30995},"八切换到异步线程或手动创建线程","八、切换到异步线程或手动创建线程",[18,30998,30999,31000,31003],{},"传统的 ",[22,31001,31002],{},"PlatformTransactionManager"," 通常把事务资源绑定到当前线程。事务方法中启动的新线程，不会自动继承原线程的事务：",[144,31005,31007],{"className":197,"code":31006,"language":199,"meta":150,"style":150},"@Transactional\npublic void createOrder() {\n    orderRepository.save(order);\n\n    new Thread(() ->\n            inventoryRepository.deduct(stock)\n    ).start();\n}\n",[22,31008,31009,31015,31025,31034,31038,31049,31059,31069],{"__ignoreMap":150},[203,31010,31011,31013],{"class":205,"line":206},[203,31012,20011],{"class":219},[203,31014,29362],{"class":209},[203,31016,31017,31019,31021,31023],{"class":205,"line":251},[203,31018,441],{"class":209},[203,31020,567],{"class":209},[203,31022,29371],{"class":215},[203,31024,18242],{"class":219},[203,31026,31027,31030,31032],{"class":205,"line":260},[203,31028,31029],{"class":219},"    orderRepository.",[203,31031,30620],{"class":215},[203,31033,30868],{"class":219},[203,31035,31036],{"class":205,"line":278},[203,31037,757],{"emptyLinePlaceholder":756},[203,31039,31040,31043,31045,31047],{"class":205,"line":285},[203,31041,31042],{"class":209},"    new",[203,31044,18025],{"class":215},[203,31046,781],{"class":219},[203,31048,784],{"class":209},[203,31050,31051,31054,31056],{"class":205,"line":541},[203,31052,31053],{"class":219},"            inventoryRepository.",[203,31055,30876],{"class":215},[203,31057,31058],{"class":219},"(stock)\n",[203,31060,31061,31064,31067],{"class":205,"line":549},[203,31062,31063],{"class":219},"    ).",[203,31065,31066],{"class":215},"start",[203,31068,257],{"class":219},[203,31070,31071],{"class":205,"line":787},[203,31072,288],{"class":219},[18,31074,31075],{},"这里的订单写入和库存扣减不在同一个本地事务中。即使外层事务回滚，也不能自动回滚新线程已经完成的数据库操作。",[18,31077,31078,31080],{},[22,31079,19845],{}," 同样会切换执行线程。可以让异步方法在自己的线程中重新开启一个独立事务，但这仍然不是对调用方事务的继承：",[144,31082,31085],{"className":31083,"code":31084,"language":149,"meta":150},[147],"调用线程事务 A\n        ↓ 提交异步任务\n异步线程事务 B\n",[22,31086,31084],{"__ignoreMap":150},[18,31088,31089],{},"跨线程、跨进程的一致性通常需要消息、Outbox、补偿或状态机等方案，而不能依赖一个线程绑定的本地事务自然传播。",[14,31091,31093],{"id":31092},"九事务传播行为与预期不一致","九、事务传播行为与预期不一致",[18,31095,31096],{},"传播行为决定事务方法被调用时，是加入已有事务、创建新事务，还是暂停或拒绝事务。",[18,31098,31099],{},"常见配置包括：",[299,31101,31102,31115],{},[302,31103,31104],{},[305,31105,31106,31109,31112],{},[308,31107,31108],{},"传播行为",[308,31110,31111],{},"主要语义",[308,31113,31114],{},"容易产生的误解",[315,31116,31117,31130,31143,31156,31169],{},[305,31118,31119,31124,31127],{},[320,31120,31121],{},[22,31122,31123],{},"REQUIRED",[320,31125,31126],{},"有事务就加入，没有就创建",[320,31128,31129],{},"内外层通常属于同一个物理事务",[305,31131,31132,31137,31140],{},[320,31133,31134],{},[22,31135,31136],{},"REQUIRES_NEW",[320,31138,31139],{},"暂停外层事务，创建独立事务",[320,31141,31142],{},"内层提交后，外层回滚不会撤销内层提交",[305,31144,31145,31150,31153],{},[320,31146,31147],{},[22,31148,31149],{},"NESTED",[320,31151,31152],{},"在已有事务中创建保存点",[320,31154,31155],{},"依赖事务管理器和数据库对保存点的支持",[305,31157,31158,31163,31166],{},[320,31159,31160],{},[22,31161,31162],{},"NOT_SUPPORTED",[320,31164,31165],{},"暂停已有事务，以非事务方式执行",[320,31167,31168],{},"方法中的操作不会跟随外层事务回滚",[305,31170,31171,31176,31179],{},[320,31172,31173],{},[22,31174,31175],{},"NEVER",[320,31177,31178],{},"存在事务时直接抛出异常",[320,31180,31181],{},"不是“不开新事务后继续执行”",[18,31183,686],{},[144,31185,31187],{"className":197,"code":31186,"language":199,"meta":150,"style":150},"@Transactional(propagation = Propagation.REQUIRES_NEW)\npublic void writeAuditLog() {\n    auditRepository.save(log);\n}\n",[22,31188,31189,31205,31216,31226],{"__ignoreMap":150},[203,31190,31191,31193,31195,31197,31200,31202],{"class":205,"line":206},[203,31192,20011],{"class":219},[203,31194,30693],{"class":209},[203,31196,220],{"class":219},[203,31198,31199],{"class":718},"propagation",[203,31201,9514],{"class":209},[203,31203,31204],{"class":219}," Propagation.REQUIRES_NEW)\n",[203,31206,31207,31209,31211,31214],{"class":205,"line":251},[203,31208,441],{"class":209},[203,31210,567],{"class":209},[203,31212,31213],{"class":215}," writeAuditLog",[203,31215,18242],{"class":219},[203,31217,31218,31221,31223],{"class":205,"line":260},[203,31219,31220],{"class":219},"    auditRepository.",[203,31222,30620],{"class":215},[203,31224,31225],{"class":219},"(log);\n",[203,31227,31228],{"class":205,"line":278},[203,31229,288],{"class":219},[18,31231,31232],{},"如果调用确实经过另一个 Bean 的代理，审计日志使用独立事务。它提交后，即使外层业务事务回滚，日志仍然保留。这是传播行为的正常语义，不是事务失效。",[18,31234,31235],{},"还要注意，同类自调用时，传播配置同样会因为绕过代理而无法生效。",[14,31237,31239],{"id":31238},"十多数据源使用了错误的事务管理器","十、多数据源使用了错误的事务管理器",[18,31241,31242],{},"应用存在多个数据源时，通常也存在多个事务管理器：",[144,31244,31246],{"className":197,"code":31245,"language":199,"meta":150,"style":150},"@Transactional(transactionManager = \"orderTransactionManager\")\npublic void createOrder() {\n    \u002F\u002F orderDataSource 操作\n}\n",[22,31247,31248,31266,31276,31281],{"__ignoreMap":150},[203,31249,31250,31252,31254,31256,31259,31261,31264],{"class":205,"line":206},[203,31251,20011],{"class":219},[203,31253,30693],{"class":209},[203,31255,220],{"class":219},[203,31257,31258],{"class":718},"transactionManager",[203,31260,9514],{"class":209},[203,31262,31263],{"class":223}," \"orderTransactionManager\"",[203,31265,12343],{"class":219},[203,31267,31268,31270,31272,31274],{"class":205,"line":251},[203,31269,441],{"class":209},[203,31271,567],{"class":209},[203,31273,29371],{"class":215},[203,31275,18242],{"class":219},[203,31277,31278],{"class":205,"line":260},[203,31279,31280],{"class":281},"    \u002F\u002F orderDataSource 操作\n",[203,31282,31283],{"class":205,"line":278},[203,31284,288],{"class":219},[18,31286,31287],{},"如果方法实际操作库存数据源，却使用订单数据源对应的事务管理器，库存连接可能没有加入当前事务。常见现象是订单表回滚了，库存表却已经提交。",[18,31289,31290],{},"排查时需要确认：",[159,31292,31293,31298,31301,31304],{},[162,31294,31295,31297],{},[22,31296,29216],{}," 选择的是哪个事务管理器；",[162,31299,31300],{},"Repository、Mapper 或 JdbcTemplate 使用的是哪个数据源；",[162,31302,31303],{},"参与操作的连接是否由当前事务管理器管理；",[162,31305,31306],{},"是否误以为一个本地事务能够覆盖多个独立数据库。",[18,31308,31309],{},"一个普通的本地事务管理器只能管理对应资源。多个独立数据库需要根据一致性要求选择分布式事务、消息最终一致性或业务补偿方案。",[14,31311,31313],{"id":31312},"十一底层存储或操作不支持事务","十一、底层存储或操作不支持事务",[18,31315,31316],{},"Spring 能管理事务，不代表所有底层操作都具备事务能力。例如：",[159,31318,31319,31322,31325,31328,31331],{},[162,31320,31321],{},"使用不支持事务的数据库存储引擎；",[162,31323,31324],{},"在事务中调用远程 HTTP 接口；",[162,31326,31327],{},"发送不受当前事务管理器协调的普通消息；",[162,31329,31330],{},"操作另一个没有加入当前事务的连接或数据源；",[162,31332,31333],{},"数据库 DDL 的提交行为与普通 DML 不同。",[18,31335,31336,31337,31339],{},"这些操作即使写在 ",[22,31338,29216],{}," 方法中，也不一定能随着数据库事务一起回滚。",[18,31341,31342],{},"例如下面的远程调用不会因为本地数据库回滚而自动撤销：",[144,31344,31346],{"className":197,"code":31345,"language":199,"meta":150,"style":150},"@Transactional\npublic void createOrder() {\n    orderRepository.save(order);\n    paymentClient.createPayment(order);\n    throw new IllegalStateException(\"创建失败\");\n}\n",[22,31347,31348,31354,31364,31372,31382,31397],{"__ignoreMap":150},[203,31349,31350,31352],{"class":205,"line":206},[203,31351,20011],{"class":219},[203,31353,29362],{"class":209},[203,31355,31356,31358,31360,31362],{"class":205,"line":251},[203,31357,441],{"class":209},[203,31359,567],{"class":209},[203,31361,29371],{"class":215},[203,31363,18242],{"class":219},[203,31365,31366,31368,31370],{"class":205,"line":260},[203,31367,31029],{"class":219},[203,31369,30620],{"class":215},[203,31371,30868],{"class":219},[203,31373,31374,31377,31380],{"class":205,"line":278},[203,31375,31376],{"class":219},"    paymentClient.",[203,31378,31379],{"class":215},"createPayment",[203,31381,30868],{"class":219},[203,31383,31384,31386,31388,31390,31392,31395],{"class":205,"line":285},[203,31385,1488],{"class":209},[203,31387,212],{"class":209},[203,31389,1038],{"class":215},[203,31391,220],{"class":219},[203,31393,31394],{"class":223},"\"创建失败\"",[203,31396,227],{"class":219},[203,31398,31399],{"class":205,"line":541},[203,31400,288],{"class":219},[18,31402,31403],{},"本地订单可以回滚，但远程支付系统已经完成的操作需要对方提供撤销接口或通过补偿机制处理。",[14,31405,31407],{"id":31406},"十二代理类型或注解位置不合适","十二、代理类型或注解位置不合适",[18,31409,31410,31411,31413],{},"Spring AOP 常见代理包括 JDK 动态代理和基于类的代理。把 ",[22,31412,29216],{}," 放在具体实现类的 public 方法上，通常最直观、最稳定：",[144,31415,31417],{"className":197,"code":31416,"language":199,"meta":150,"style":150},"@Service\npublic class OrderServiceImpl implements OrderService {\n\n    @Override\n    @Transactional\n    public void createOrder() {\n        \u002F\u002F 数据库操作\n    }\n}\n",[22,31418,31419,31425,31440,31444,31450,31456,31466,31470,31474],{"__ignoreMap":150},[203,31420,31421,31423],{"class":205,"line":206},[203,31422,20011],{"class":219},[203,31424,26541],{"class":209},[203,31426,31427,31429,31431,31434,31436,31438],{"class":205,"line":251},[203,31428,441],{"class":209},[203,31430,1536],{"class":209},[203,31432,31433],{"class":215}," OrderServiceImpl",[203,31435,20788],{"class":209},[203,31437,29315],{"class":215},[203,31439,248],{"class":219},[203,31441,31442],{"class":205,"line":260},[203,31443,757],{"emptyLinePlaceholder":756},[203,31445,31446,31448],{"class":205,"line":278},[203,31447,20042],{"class":219},[203,31449,20045],{"class":209},[203,31451,31452,31454],{"class":205,"line":285},[203,31453,20042],{"class":219},[203,31455,29362],{"class":209},[203,31457,31458,31460,31462,31464],{"class":205,"line":541},[203,31459,1555],{"class":209},[203,31461,567],{"class":209},[203,31463,29371],{"class":215},[203,31465,18242],{"class":219},[203,31467,31468],{"class":205,"line":549},[203,31469,29378],{"class":281},[203,31471,31472],{"class":205,"line":787},[203,31473,751],{"class":219},[203,31475,31476],{"class":205,"line":799},[203,31477,288],{"class":219},[18,31479,31480],{},"只在接口上放注解可能受到代理方式、注解查找方式和 AspectJ 模式影响。Spring 官方更推荐把事务注解放在具体类的方法上，避免更换代理或织入方式后出现差异。",[14,31482,15705],{"id":15704},[473,31484,31486],{"id":31485},"_1-确认是否真的没有事务","1. 确认是否真的没有事务",[18,31488,31489],{},"不要只根据“数据库数据没回滚”下结论。先确认：",[159,31491,31492,31495,31498,31501],{},[162,31493,31494],{},"方法是否经过事务代理；",[162,31496,31497],{},"是否创建或加入了事务；",[162,31499,31500],{},"最终执行了提交还是回滚；",[162,31502,31503],{},"没回滚的操作是否属于同一个事务资源。",[18,31505,31506],{},"可以临时提高 Spring 事务相关日志级别，并结合数据库连接、SQL 日志和调用链判断。",[473,31508,31510],{"id":31509},"_2-检查代理入口","2. 检查代理入口",[18,31512,31513],{},"依次确认：",[144,31515,31518],{"className":31516,"code":31517,"language":149,"meta":150},[147],"对象是不是 Spring Bean\n    ↓\n调用是不是从其他 Bean 进入\n    ↓\n方法是否可被当前代理方式增强\n    ↓\n是否发生在 Bean 初始化阶段\n",[22,31519,31517],{"__ignoreMap":150},[473,31521,31523],{"id":31522},"_3-检查回滚条件","3. 检查回滚条件",[18,31525,22749],{},[144,31527,31530],{"className":31528,"code":31529,"language":149,"meta":150},[147],"实际抛出的异常类型\nrollbackFor \u002F noRollbackFor\n异常是否在方法内部被捕获\n异常是否在事务方法返回后才发生\n",[22,31531,31529],{"__ignoreMap":150},[18,31533,31534],{},"事务方法已经正常返回并提交后，调用方后续再抛异常，不能回头撤销已经提交的事务。",[473,31536,31538],{"id":31537},"_4-检查事务边界","4. 检查事务边界",[18,31540,31541],{},"继续确认：",[159,31543,31544,31547,31555,31558],{},[162,31545,31546],{},"是否切换线程；",[162,31548,31549,31550,53,31552,31554],{},"是否使用 ",[22,31551,31136],{},[22,31553,31162],{}," 等传播行为；",[162,31556,31557],{},"是否跨数据源或跨数据库；",[162,31559,31560],{},"是否包含远程调用、消息或其他非事务资源。",[14,31562,1993],{"id":1992},[299,31564,31565,31574],{},[302,31566,31567],{},[305,31568,31569,31571],{},[308,31570,22910],{},[308,31572,31573],{},"首先检查",[315,31575,31576,31584,31595,31603,31612,31623,31631],{},[305,31577,31578,31581],{},[320,31579,31580],{},"方法完全没有事务日志",[320,31582,31583],{},"对象是否为 Spring Bean，调用是否经过代理",[305,31585,31586,31589],{},[320,31587,31588],{},"同类方法上的注解不生效",[320,31590,31591,31592,31594],{},"是否通过 ",[22,31593,2286],{}," 自调用",[305,31596,31597,31600],{},[320,31598,31599],{},"抛异常但数据仍提交",[320,31601,31602],{},"异常是否为受检异常，是否被捕获",[305,31604,31605,31608],{},[320,31606,31607],{},"外层回滚但内层数据保留",[320,31609,31549,31610],{},[22,31611,31136],{},[305,31613,31614,31617],{},[320,31615,31616],{},"主线程回滚但异步操作成功",[320,31618,31619,31620,31622],{},"是否切换到 ",[22,31621,19845],{}," 或新线程",[305,31624,31625,31628],{},[320,31626,31627],{},"一个库回滚、另一个库提交",[320,31629,31630],{},"事务管理器和数据源是否匹配",[305,31632,31633,31636],{},[320,31634,31635],{},"数据库回滚但远程操作没撤销",[320,31637,31638],{},"远程调用不属于本地事务",[14,31640,2071],{"id":2070},[159,31642,31643,31650,31657,31664,31671],{},[162,31644,31645],{},[119,31646,31649],{"href":31647,"rel":31648},"https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Freference\u002Fdata-access\u002Ftransaction\u002Fdeclarative\u002Ftx-decl-explained.html",[2080],"Spring 声明式事务实现原理",[162,31651,31652],{},[119,31653,31656],{"href":31654,"rel":31655},"https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Freference\u002Fdata-access\u002Ftransaction\u002Fdeclarative\u002Fannotations.html",[2080],"Spring @Transactional 使用说明",[162,31658,31659],{},[119,31660,31663],{"href":31661,"rel":31662},"https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Freference\u002Fdata-access\u002Ftransaction\u002Fdeclarative\u002Frolling-back.html",[2080],"Spring 声明式事务回滚规则",[162,31665,31666],{},[119,31667,31670],{"href":31668,"rel":31669},"https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Freference\u002Fcore\u002Faop\u002Fproxying.html",[2080],"Spring AOP 代理机制",[162,31672,31673],{},[119,31674,31677],{"href":31675,"rel":31676},"https:\u002F\u002Fdocs.spring.io\u002Fspring-framework\u002Freference\u002Fdata-access\u002Ftransaction\u002Fdeclarative\u002Ftx-propagation.html",[2080],"Spring 事务传播行为",[2111,31679,31680],{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":150,"searchDepth":251,"depth":251,"links":31682},[31683,31684,31685,31686,31687,31688,31689,31695,31696,31697,31698,31699,31700,31701,31702,31703,31709,31710],{"id":16,"depth":251,"text":16},{"id":115,"depth":251,"text":115},{"id":132,"depth":251,"text":132},{"id":29261,"depth":251,"text":29262},{"id":29294,"depth":251,"text":29295},{"id":29448,"depth":251,"text":29449},{"id":30088,"depth":251,"text":30089,"children":31690},[31691,31692,31693,31694],{"id":30095,"depth":260,"text":30096},{"id":30127,"depth":260,"text":30128},{"id":30164,"depth":260,"text":30165},{"id":30198,"depth":260,"text":30199},{"id":30217,"depth":251,"text":30218},{"id":30508,"depth":251,"text":30509},{"id":30826,"depth":251,"text":30827},{"id":30995,"depth":251,"text":30996},{"id":31092,"depth":251,"text":31093},{"id":31238,"depth":251,"text":31239},{"id":31312,"depth":251,"text":31313},{"id":31406,"depth":251,"text":31407},{"id":15704,"depth":251,"text":15705,"children":31704},[31705,31706,31707,31708],{"id":31485,"depth":260,"text":31486},{"id":31509,"depth":260,"text":31510},{"id":31522,"depth":260,"text":31523},{"id":31537,"depth":260,"text":31538},{"id":1992,"depth":251,"text":1993},{"id":2070,"depth":251,"text":2071},"wiki:java:spring-transaction-failure-scenarios","从事务代理、同类调用、方法可代理性、异常回滚规则、异步线程、传播行为、多数据源和数据库能力等角度，系统讲解 Spring 声明式事务不生效的常见原因。",{},85,"\u002Fjava\u002Fspring-transaction-failure-scenarios",{"title":29199,"description":31712},"java\u002Fspring-transaction-failure-scenarios","MbQ4RJKsR0kpBNXHiG5krX5FNjXdp01iF5-tKVP3kEU",{"id":4,"title":5,"body":31720,"commentId":2145,"description":2146,"difficulty":2147,"draft":2148,"extension":2149,"meta":33408,"navigation":756,"order":2151,"path":2152,"section":2153,"seo":33409,"stem":2155,"updated":2156,"__hash__":2157},{"type":7,"value":31721,"toc":33377},[31722,31724,31726,31740,31752,31774,31776,31782,31784,31791,31793,31795,31799,31804,31808,31828,31832,31850,31856,31892,31894,31898,31952,31960,31962,31966,31971,31975,31977,31981,31983,31985,31993,31997,32025,32027,32029,32089,32091,32125,32127,32129,32189,32191,32193,32199,32201,32287,32289,32381,32383,32385,32387,32392,32396,32401,32461,32463,32467,32485,32489,32517,32519,32524,32526,32530,32586,32588,32640,32642,32686,32688,32692,32774,32780,32812,32818,32826,32828,32830,32892,32894,32956,32960,32962,32994,32996,32998,33000,33056,33058,33060,33124,33126,33132,33134,33138,33208,33210,33212,33214,33246,33248,33250,33276,33278,33280,33282,33284,33286,33288,33346,33348,33375],[10,31723,5],{"id":12},[14,31725,16],{"id":16},[18,31727,20,31728,25,31730,29,31732,33,31734,36,31736,40,31738,44],{},[22,31729,24],{},[22,31731,28],{},[22,31733,32],{},[22,31735,32],{},[22,31737,39],{},[22,31739,43],{},[18,31741,31742,49,31744,53,31746,40,31748,60,31750,63],{},[22,31743,39],{},[22,31745,52],{},[22,31747,56],{},[22,31749,59],{},[22,31751,43],{},[18,31753,66,31754,70,31756,73,31758,53,31760,53,31762,83,31764,87,31766,90,31768,53,31770,53,31772,100],{},[22,31755,69],{},[22,31757,43],{},[22,31759,76],{},[22,31761,79],{},[22,31763,82],{},[22,31765,86],{},[22,31767,69],{},[22,31769,93],{},[22,31771,96],{},[22,31773,99],{},[18,31775,103],{},[105,31777,31778],{},[18,31779,31780],{},[110,31781,112],{},[14,31783,115],{"id":115},[18,31785,31786],{},[119,31787,31789],{"href":121,"target":122,"rel":31788},[124,125],[127,31790],{"src":121,"alt":129},[14,31792,132],{"id":132},[14,31794,136],{"id":135},[18,31796,139,31797,142],{},[22,31798,32],{},[144,31800,31802],{"className":31801,"code":148,"language":149,"meta":150},[147],[22,31803,148],{"__ignoreMap":150},[18,31805,31806,157],{},[22,31807,32],{},[159,31809,31810,31814,31818,31822],{},[162,31811,31812,167],{},[22,31813,166],{},[162,31815,31816,173],{},[22,31817,172],{},[162,31819,31820,179],{},[22,31821,178],{},[162,31823,31824,185,31826,33],{},[22,31825,184],{},[22,31827,188],{},[18,31829,191,31830,194],{},[22,31831,32],{},[144,31833,31834],{"className":197,"code":198,"language":199,"meta":150,"style":150},[22,31835,31836],{"__ignoreMap":150},[203,31837,31838,31840,31842,31844,31846,31848],{"class":205,"line":206},[203,31839,24],{"class":209},[203,31841,212],{"class":209},[203,31843,216],{"class":215},[203,31845,220],{"class":219},[203,31847,224],{"class":223},[203,31849,227],{"class":219},[18,31851,31852,232,31854,235],{},[22,31853,28],{},[22,31855,32],{},[144,31857,31858],{"className":197,"code":238,"language":199,"meta":150,"style":150},[22,31859,31860,31866,31872,31884,31888],{"__ignoreMap":150},[203,31861,31862,31864],{"class":205,"line":206},[203,31863,245],{"class":209},[203,31865,248],{"class":219},[203,31867,31868,31870],{"class":205,"line":251},[203,31869,254],{"class":215},[203,31871,257],{"class":219},[203,31873,31874,31876,31878,31880,31882],{"class":205,"line":260},[203,31875,263],{"class":219},[203,31877,28],{"class":209},[203,31879,268],{"class":219},[203,31881,272],{"class":271},[203,31883,275],{"class":219},[203,31885,31886],{"class":205,"line":278},[203,31887,282],{"class":281},[203,31889,31890],{"class":205,"line":285},[203,31891,288],{"class":219},[14,31893,292],{"id":291},[18,31895,31896,297],{},[22,31897,39],{},[299,31899,31900,31908],{},[302,31901,31902],{},[305,31903,31904,31906],{},[308,31905,310],{},[308,31907,313],{},[315,31909,31910,31918,31926,31934,31942],{},[305,31911,31912,31916],{},[320,31913,31914],{},[22,31915,52],{},[320,31917,326],{},[305,31919,31920,31924],{},[320,31921,31922],{},[22,31923,56],{},[320,31925,335],{},[305,31927,31928,31932],{},[320,31929,31930],{},[22,31931,59],{},[320,31933,344],{},[305,31935,31936,31940],{},[320,31937,31938],{},[22,31939,351],{},[320,31941,354],{},[305,31943,31944,31948],{},[320,31945,31946],{},[22,31947,361],{},[320,31949,31950,367],{},[22,31951,366],{},[18,31953,31954,372,31956,375,31958,378],{},[22,31955,39],{},[22,31957,28],{},[22,31959,52],{},[14,31961,382],{"id":381},[18,31963,31964,387],{},[22,31965,43],{},[144,31967,31969],{"className":31968,"code":391,"language":149,"meta":150},[147],[22,31970,391],{"__ignoreMap":150},[18,31972,396,31973,399],{},[22,31974,69],{},[18,31976,402],{},[105,31978,31979],{},[18,31980,407],{},[14,31982,411],{"id":410},[18,31984,414],{},[105,31986,31987],{},[18,31988,419,31989,422,31991,425],{},[22,31990,43],{},[22,31992,69],{},[18,31994,428,31995,431],{},[22,31996,76],{},[144,31998,31999],{"className":197,"code":434,"language":199,"meta":150,"style":150},[22,32000,32001,32011,32021],{"__ignoreMap":150},[203,32002,32003,32005,32007,32009],{"class":205,"line":206},[203,32004,441],{"class":209},[203,32006,444],{"class":219},[203,32008,447],{"class":215},[203,32010,450],{"class":219},[203,32012,32013,32015,32017,32019],{"class":205,"line":251},[203,32014,455],{"class":209},[203,32016,458],{"class":219},[203,32018,461],{"class":215},[203,32020,464],{"class":219},[203,32022,32023],{"class":205,"line":260},[203,32024,288],{"class":219},[18,32026,471],{},[473,32028,476],{"id":475},[144,32030,32031],{"className":197,"code":479,"language":199,"meta":150,"style":150},[22,32032,32033,32039,32049,32055,32067,32079,32085],{"__ignoreMap":150},[203,32034,32035,32037],{"class":205,"line":206},[203,32036,245],{"class":209},[203,32038,248],{"class":219},[203,32040,32041,32043,32045,32047],{"class":205,"line":251},[203,32042,492],{"class":219},[203,32044,495],{"class":209},[203,32046,498],{"class":215},[203,32048,501],{"class":219},[203,32050,32051,32053],{"class":205,"line":260},[203,32052,506],{"class":215},[203,32054,509],{"class":219},[203,32056,32057,32059,32061,32063,32065],{"class":205,"line":278},[203,32058,263],{"class":219},[203,32060,28],{"class":209},[203,32062,518],{"class":219},[203,32064,272],{"class":271},[203,32066,275],{"class":219},[203,32068,32069,32071,32073,32075,32077],{"class":205,"line":285},[203,32070,527],{"class":219},[203,32072,530],{"class":215},[203,32074,220],{"class":219},[203,32076,535],{"class":223},[203,32078,538],{"class":219},[203,32080,32081,32083],{"class":205,"line":541},[203,32082,544],{"class":215},[203,32084,257],{"class":219},[203,32086,32087],{"class":205,"line":549},[203,32088,288],{"class":219},[473,32090,555],{"id":554},[144,32092,32093],{"className":197,"code":558,"language":199,"meta":150,"style":150},[22,32094,32095,32105,32115,32121],{"__ignoreMap":150},[203,32096,32097,32099,32101,32103],{"class":205,"line":206},[203,32098,441],{"class":209},[203,32100,567],{"class":209},[203,32102,570],{"class":215},[203,32104,450],{"class":219},[203,32106,32107,32109,32111,32113],{"class":205,"line":251},[203,32108,492],{"class":219},[203,32110,495],{"class":209},[203,32112,498],{"class":215},[203,32114,501],{"class":219},[203,32116,32117,32119],{"class":205,"line":260},[203,32118,506],{"class":215},[203,32120,509],{"class":219},[203,32122,32123],{"class":205,"line":278},[203,32124,288],{"class":219},[18,32126,595],{},[18,32128,598],{},[299,32130,32131,32139],{},[302,32132,32133],{},[305,32134,32135,32137],{},[308,32136,310],{},[308,32138,609],{},[315,32140,32141,32149,32157,32165,32173,32181],{},[305,32142,32143,32147],{},[320,32144,32145],{},[22,32146,76],{},[320,32148,620],{},[305,32150,32151,32155],{},[320,32152,32153],{},[22,32154,79],{},[320,32156,629],{},[305,32158,32159,32163],{},[320,32160,32161],{},[22,32162,82],{},[320,32164,638],{},[305,32166,32167,32171],{},[320,32168,32169],{},[22,32170,645],{},[320,32172,648],{},[305,32174,32175,32179],{},[320,32176,32177],{},[22,32178,655],{},[320,32180,658],{},[305,32182,32183,32187],{},[320,32184,32185],{},[22,32186,665],{},[320,32188,668],{},[18,32190,671],{},[14,32192,675],{"id":674},[18,32194,32195,680,32197,683],{},[22,32196,69],{},[22,32198,86],{},[18,32200,686],{},[144,32202,32203],{"className":197,"code":689,"language":199,"meta":150,"style":150},[22,32204,32205,32215,32233,32247,32251,32255,32265,32275,32283],{"__ignoreMap":150},[203,32206,32207,32209,32211,32213],{"class":205,"line":206},[203,32208,441],{"class":209},[203,32210,698],{"class":219},[203,32212,701],{"class":215},[203,32214,704],{"class":219},[203,32216,32217,32219,32221,32223,32225,32227,32229,32231],{"class":205,"line":251},[203,32218,709],{"class":209},[203,32220,712],{"class":219},[203,32222,715],{"class":209},[203,32224,719],{"class":718},[203,32226,722],{"class":209},[203,32228,725],{"class":219},[203,32230,728],{"class":215},[203,32232,731],{"class":219},[203,32234,32235,32237,32239,32241,32243,32245],{"class":205,"line":260},[203,32236,736],{"class":209},[203,32238,212],{"class":209},[203,32240,216],{"class":215},[203,32242,220],{"class":219},[203,32244,224],{"class":223},[203,32246,227],{"class":219},[203,32248,32249],{"class":205,"line":278},[203,32250,751],{"class":219},[203,32252,32253],{"class":205,"line":285},[203,32254,757],{"emptyLinePlaceholder":756},[203,32256,32257,32259,32261,32263],{"class":205,"line":541},[203,32258,455],{"class":209},[203,32260,764],{"class":219},[203,32262,767],{"class":215},[203,32264,770],{"class":219},[203,32266,32267,32269,32271,32273],{"class":205,"line":549},[203,32268,775],{"class":219},[203,32270,778],{"class":215},[203,32272,781],{"class":219},[203,32274,784],{"class":209},[203,32276,32277,32279,32281],{"class":205,"line":787},[203,32278,790],{"class":209},[203,32280,793],{"class":215},[203,32282,796],{"class":219},[203,32284,32285],{"class":205,"line":799},[203,32286,288],{"class":219},[18,32288,804],{},[299,32290,32291,32299],{},[302,32292,32293],{},[305,32294,32295,32297],{},[308,32296,310],{},[308,32298,313],{},[315,32300,32301,32309,32317,32325,32333,32341,32349,32357,32365,32373],{},[305,32302,32303,32307],{},[320,32304,32305],{},[22,32306,93],{},[320,32308,825],{},[305,32310,32311,32315],{},[320,32312,32313],{},[22,32314,96],{},[320,32316,834],{},[305,32318,32319,32323],{},[320,32320,32321],{},[22,32322,841],{},[320,32324,844],{},[305,32326,32327,32331],{},[320,32328,32329],{},[22,32330,99],{},[320,32332,853],{},[305,32334,32335,32339],{},[320,32336,32337],{},[22,32338,860],{},[320,32340,863],{},[305,32342,32343,32347],{},[320,32344,32345],{},[22,32346,870],{},[320,32348,873],{},[305,32350,32351,32355],{},[320,32352,32353],{},[22,32354,880],{},[320,32356,883],{},[305,32358,32359,32363],{},[320,32360,32361],{},[22,32362,890],{},[320,32364,893],{},[305,32366,32367,32371],{},[320,32368,32369],{},[22,32370,900],{},[320,32372,903],{},[305,32374,32375,32379],{},[320,32376,32377],{},[22,32378,910],{},[320,32380,913],{},[18,32382,916],{},[14,32384,920],{"id":919},[18,32386,923],{},[144,32388,32390],{"className":32389,"code":927,"language":149,"meta":150},[147],[22,32391,927],{"__ignoreMap":150},[18,32393,932,32394,935],{},[22,32395,32],{},[144,32397,32399],{"className":32398,"code":939,"language":149,"meta":150},[147],[22,32400,939],{"__ignoreMap":150},[299,32402,32403,32413],{},[302,32404,32405],{},[305,32406,32407,32409,32411],{},[308,32408,950],{},[308,32410,953],{},[308,32412,956],{},[315,32414,32415,32429,32437,32445,32453],{},[305,32416,32417,32419,32423],{},[320,32418,963],{},[320,32420,32421],{},[22,32422,43],{},[320,32424,32425,53,32427],{},[22,32426,69],{},[22,32428,39],{},[305,32430,32431,32433,32435],{},[320,32432,978],{},[320,32434,981],{},[320,32436,984],{},[305,32438,32439,32441,32443],{},[320,32440,989],{},[320,32442,992],{},[320,32444,992],{},[305,32446,32447,32449,32451],{},[320,32448,999],{},[320,32450,992],{},[320,32452,1004],{},[305,32454,32455,32457,32459],{},[320,32456,1009],{},[320,32458,1012],{},[320,32460,1015],{},[14,32462,1019],{"id":1018},[18,32464,32465,1024],{},[22,32466,24],{},[144,32468,32469],{"className":197,"code":1027,"language":199,"meta":150,"style":150},[22,32470,32471],{"__ignoreMap":150},[203,32472,32473,32475,32477,32479,32481,32483],{"class":205,"line":206},[203,32474,24],{"class":209},[203,32476,212],{"class":209},[203,32478,1038],{"class":215},[203,32480,220],{"class":219},[203,32482,1043],{"class":223},[203,32484,227],{"class":219},[18,32486,32487,1050],{},[22,32488,86],{},[144,32490,32491],{"className":197,"code":1053,"language":199,"meta":150,"style":150},[22,32492,32493,32503,32513],{"__ignoreMap":150},[203,32494,32495,32497,32499,32501],{"class":205,"line":206},[203,32496,441],{"class":209},[203,32498,444],{"class":219},[203,32500,1064],{"class":215},[203,32502,450],{"class":219},[203,32504,32505,32507,32509,32511],{"class":205,"line":251},[203,32506,455],{"class":209},[203,32508,458],{"class":219},[203,32510,461],{"class":215},[203,32512,501],{"class":219},[203,32514,32515],{"class":205,"line":260},[203,32516,288],{"class":219},[18,32518,1083],{},[144,32520,32522],{"className":32521,"code":1087,"language":149,"meta":150},[147],[22,32523,1087],{"__ignoreMap":150},[14,32525,1093],{"id":1092},[18,32527,1096,32528,431],{},[22,32529,28],{},[144,32531,32532],{"className":197,"code":1101,"language":199,"meta":150,"style":150},[22,32533,32534,32540,32546,32558,32564,32576,32582],{"__ignoreMap":150},[203,32535,32536,32538],{"class":205,"line":206},[203,32537,245],{"class":209},[203,32539,248],{"class":219},[203,32541,32542,32544],{"class":205,"line":251},[203,32543,1114],{"class":215},[203,32545,1117],{"class":219},[203,32547,32548,32550,32552,32554,32556],{"class":205,"line":260},[203,32549,263],{"class":219},[203,32551,28],{"class":209},[203,32553,268],{"class":219},[203,32555,272],{"class":271},[203,32557,275],{"class":219},[203,32559,32560,32562],{"class":205,"line":278},[203,32561,1134],{"class":215},[203,32563,1137],{"class":219},[203,32565,32566,32568,32570,32572,32574],{"class":205,"line":285},[203,32567,263],{"class":219},[203,32569,28],{"class":209},[203,32571,1146],{"class":219},[203,32573,272],{"class":271},[203,32575,275],{"class":219},[203,32577,32578,32580],{"class":205,"line":541},[203,32579,1155],{"class":215},[203,32581,1137],{"class":219},[203,32583,32584],{"class":205,"line":549},[203,32585,288],{"class":219},[18,32587,1164],{},[144,32589,32590],{"className":197,"code":1167,"language":199,"meta":150,"style":150},[22,32591,32592,32598,32604,32616,32620,32632,32636],{"__ignoreMap":150},[203,32593,32594,32596],{"class":205,"line":206},[203,32595,245],{"class":209},[203,32597,248],{"class":219},[203,32599,32600,32602],{"class":205,"line":251},[203,32601,1114],{"class":215},[203,32603,1117],{"class":219},[203,32605,32606,32608,32610,32612,32614],{"class":205,"line":260},[203,32607,263],{"class":219},[203,32609,28],{"class":209},[203,32611,1146],{"class":219},[203,32613,272],{"class":271},[203,32615,275],{"class":219},[203,32617,32618],{"class":205,"line":278},[203,32619,1198],{"class":281},[203,32621,32622,32624,32626,32628,32630],{"class":205,"line":285},[203,32623,263],{"class":219},[203,32625,28],{"class":209},[203,32627,268],{"class":219},[203,32629,272],{"class":271},[203,32631,275],{"class":219},[203,32633,32634],{"class":205,"line":541},[203,32635,1215],{"class":281},[203,32637,32638],{"class":205,"line":549},[203,32639,288],{"class":219},[18,32641,1222],{},[144,32643,32644],{"className":197,"code":1225,"language":199,"meta":150,"style":150},[22,32645,32646,32652,32658,32670,32682],{"__ignoreMap":150},[203,32647,32648,32650],{"class":205,"line":206},[203,32649,245],{"class":209},[203,32651,248],{"class":219},[203,32653,32654,32656],{"class":205,"line":251},[203,32655,1238],{"class":215},[203,32657,257],{"class":219},[203,32659,32660,32662,32664,32666,32668],{"class":205,"line":260},[203,32661,263],{"class":219},[203,32663,28],{"class":209},[203,32665,1249],{"class":219},[203,32667,272],{"class":271},[203,32669,275],{"class":219},[203,32671,32672,32674,32676,32678,32680],{"class":205,"line":278},[203,32673,527],{"class":219},[203,32675,530],{"class":215},[203,32677,220],{"class":219},[203,32679,1264],{"class":223},[203,32681,538],{"class":219},[203,32683,32684],{"class":205,"line":285},[203,32685,288],{"class":219},[14,32687,1274],{"id":1273},[18,32689,32690,1280],{},[22,32691,1279],{},[144,32693,32694],{"className":197,"code":1283,"language":199,"meta":150,"style":150},[22,32695,32696,32706,32710,32716,32728,32738,32746,32758,32766,32770],{"__ignoreMap":150},[203,32697,32698,32700,32702,32704],{"class":205,"line":206},[203,32699,1290],{"class":219},[203,32701,495],{"class":209},[203,32703,719],{"class":718},[203,32705,1297],{"class":219},[203,32707,32708],{"class":205,"line":251},[203,32709,757],{"emptyLinePlaceholder":756},[203,32711,32712,32714],{"class":205,"line":260},[203,32713,245],{"class":209},[203,32715,248],{"class":219},[203,32717,32718,32720,32722,32724,32726],{"class":205,"line":278},[203,32719,1312],{"class":219},[203,32721,495],{"class":209},[203,32723,458],{"class":219},[203,32725,1319],{"class":215},[203,32727,501],{"class":219},[203,32729,32730,32732,32734,32736],{"class":205,"line":285},[203,32731,455],{"class":209},[203,32733,1328],{"class":219},[203,32735,1064],{"class":215},[203,32737,257],{"class":219},[203,32739,32740,32742,32744],{"class":205,"line":541},[203,32741,263],{"class":219},[203,32743,1279],{"class":209},[203,32745,248],{"class":219},[203,32747,32748,32750,32752,32754,32756],{"class":205,"line":549},[203,32749,709],{"class":209},[203,32751,1347],{"class":219},[203,32753,1350],{"class":209},[203,32755,719],{"class":718},[203,32757,275],{"class":219},[203,32759,32760,32762,32764],{"class":205,"line":787},[203,32761,1359],{"class":219},[203,32763,1362],{"class":215},[203,32765,257],{"class":219},[203,32767,32768],{"class":205,"line":799},[203,32769,751],{"class":219},[203,32771,32772],{"class":205,"line":1371},[203,32773,288],{"class":219},[18,32775,1376,32776,1380,32778,431],{},[22,32777,1379],{},[22,32779,188],{},[144,32781,32782],{"className":197,"code":1385,"language":199,"meta":150,"style":150},[22,32783,32784,32798,32808],{"__ignoreMap":150},[203,32785,32786,32788,32790,32792,32794,32796],{"class":205,"line":206},[203,32787,245],{"class":209},[203,32789,1394],{"class":219},[203,32791,495],{"class":209},[203,32793,458],{"class":219},[203,32795,1319],{"class":215},[203,32797,1403],{"class":219},[203,32799,32800,32802,32804,32806],{"class":205,"line":251},[203,32801,455],{"class":209},[203,32803,1328],{"class":219},[203,32805,1064],{"class":215},[203,32807,257],{"class":219},[203,32809,32810],{"class":205,"line":260},[203,32811,288],{"class":219},[18,32813,1420,32814,1424,32816,1428],{},[22,32815,1423],{},[22,32817,1427],{},[18,32819,1431,32820,1434,32822,1438,32824,1441],{},[22,32821,1279],{},[22,32823,1437],{},[22,32825,245],{},[14,32827,1445],{"id":1444},[18,32829,1448],{},[144,32831,32832],{"className":197,"code":1451,"language":199,"meta":150,"style":150},[22,32833,32834,32840,32850,32862,32872,32880,32884,32888],{"__ignoreMap":150},[203,32835,32836,32838],{"class":205,"line":206},[203,32837,245],{"class":209},[203,32839,248],{"class":219},[203,32841,32842,32844,32846,32848],{"class":205,"line":251},[203,32843,455],{"class":209},[203,32845,764],{"class":219},[203,32847,767],{"class":215},[203,32849,1470],{"class":219},[203,32851,32852,32854,32856,32858,32860],{"class":205,"line":260},[203,32853,263],{"class":219},[203,32855,28],{"class":209},[203,32857,1479],{"class":219},[203,32859,272],{"class":271},[203,32861,275],{"class":219},[203,32863,32864,32866,32868,32870],{"class":205,"line":278},[203,32865,1488],{"class":209},[203,32867,212],{"class":209},[203,32869,1493],{"class":215},[203,32871,1496],{"class":219},[203,32873,32874,32876,32878],{"class":205,"line":285},[203,32875,1501],{"class":223},[203,32877,1504],{"class":209},[203,32879,1507],{"class":219},[203,32881,32882],{"class":205,"line":541},[203,32883,1512],{"class":219},[203,32885,32886],{"class":205,"line":549},[203,32887,1517],{"class":219},[203,32889,32890],{"class":205,"line":787},[203,32891,288],{"class":219},[18,32893,1524],{},[144,32895,32896],{"className":197,"code":1527,"language":199,"meta":150,"style":150},[22,32897,32898,32912,32916,32924,32932,32938,32942,32948,32952],{"__ignoreMap":150},[203,32899,32900,32902,32904,32906,32908,32910],{"class":205,"line":206},[203,32901,441],{"class":209},[203,32903,1536],{"class":209},[203,32905,1493],{"class":215},[203,32907,1541],{"class":209},[203,32909,1544],{"class":215},[203,32911,248],{"class":219},[203,32913,32914],{"class":205,"line":251},[203,32915,757],{"emptyLinePlaceholder":756},[203,32917,32918,32920,32922],{"class":205,"line":260},[203,32919,1555],{"class":209},[203,32921,1493],{"class":215},[203,32923,1496],{"class":219},[203,32925,32926,32928,32930],{"class":205,"line":278},[203,32927,1564],{"class":219},[203,32929,166],{"class":271},[203,32931,1569],{"class":219},[203,32933,32934,32936],{"class":205,"line":285},[203,32935,1574],{"class":219},[203,32937,1577],{"class":271},[203,32939,32940],{"class":205,"line":541},[203,32941,1582],{"class":219},[203,32943,32944,32946],{"class":205,"line":549},[203,32945,1587],{"class":718},[203,32947,1590],{"class":219},[203,32949,32950],{"class":205,"line":787},[203,32951,751],{"class":219},[203,32953,32954],{"class":205,"line":799},[203,32955,288],{"class":219},[18,32957,1601,32958,1604],{},[22,32959,172],{},[18,32961,1607],{},[144,32963,32964],{"className":197,"code":1610,"language":199,"meta":150,"style":150},[22,32965,32966,32976,32990],{"__ignoreMap":150},[203,32967,32968,32970,32972,32974],{"class":205,"line":206},[203,32969,28],{"class":209},[203,32971,1479],{"class":219},[203,32973,272],{"class":271},[203,32975,275],{"class":219},[203,32977,32978,32980,32982,32984,32986,32988],{"class":205,"line":251},[203,32979,1488],{"class":209},[203,32981,212],{"class":209},[203,32983,1493],{"class":215},[203,32985,220],{"class":219},[203,32987,1635],{"class":223},[203,32989,227],{"class":219},[203,32991,32992],{"class":205,"line":260},[203,32993,288],{"class":219},[14,32995,1645],{"id":1644},[18,32997,1648],{},[473,32999,1652],{"id":1651},[144,33001,33002],{"className":197,"code":1655,"language":199,"meta":150,"style":150},[22,33003,33004,33012,33020,33024,33036,33048,33052],{"__ignoreMap":150},[203,33005,33006,33008,33010],{"class":205,"line":206},[203,33007,441],{"class":209},[203,33009,1536],{"class":209},[203,33011,1666],{"class":215},[203,33013,33014,33016,33018],{"class":205,"line":251},[203,33015,1671],{"class":209},[203,33017,1544],{"class":215},[203,33019,248],{"class":219},[203,33021,33022],{"class":205,"line":260},[203,33023,757],{"emptyLinePlaceholder":756},[203,33025,33026,33028,33030,33032,33034],{"class":205,"line":278},[203,33027,1555],{"class":209},[203,33029,793],{"class":215},[203,33031,1688],{"class":219},[203,33033,1691],{"class":271},[203,33035,275],{"class":219},[203,33037,33038,33040,33042,33044,33046],{"class":205,"line":285},[203,33039,1587],{"class":718},[203,33041,220],{"class":219},[203,33043,1702],{"class":223},[203,33045,1504],{"class":209},[203,33047,1707],{"class":219},[203,33049,33050],{"class":205,"line":541},[203,33051,751],{"class":219},[203,33053,33054],{"class":205,"line":549},[203,33055,288],{"class":219},[18,33057,1718],{},[473,33059,1722],{"id":1721},[144,33061,33062],{"className":197,"code":1725,"language":199,"meta":150,"style":150},[22,33063,33064,33072,33080,33084,33092,33100,33106,33110,33116,33120],{"__ignoreMap":150},[203,33065,33066,33068,33070],{"class":205,"line":206},[203,33067,441],{"class":209},[203,33069,1536],{"class":209},[203,33071,1736],{"class":215},[203,33073,33074,33076,33078],{"class":205,"line":251},[203,33075,1671],{"class":209},[203,33077,1743],{"class":215},[203,33079,248],{"class":219},[203,33081,33082],{"class":205,"line":260},[203,33083,757],{"emptyLinePlaceholder":756},[203,33085,33086,33088,33090],{"class":205,"line":278},[203,33087,1555],{"class":209},[203,33089,1756],{"class":215},[203,33091,1496],{"class":219},[203,33093,33094,33096,33098],{"class":205,"line":285},[203,33095,1564],{"class":219},[203,33097,166],{"class":271},[203,33099,1569],{"class":219},[203,33101,33102,33104],{"class":205,"line":541},[203,33103,1574],{"class":219},[203,33105,1577],{"class":271},[203,33107,33108],{"class":205,"line":549},[203,33109,1582],{"class":219},[203,33111,33112,33114],{"class":205,"line":787},[203,33113,1587],{"class":718},[203,33115,1590],{"class":219},[203,33117,33118],{"class":205,"line":799},[203,33119,751],{"class":219},[203,33121,33122],{"class":205,"line":1371},[203,33123,288],{"class":219},[18,33125,1793],{},[18,33127,1796,33128,1800,33130,1803],{},[22,33129,1799],{},[22,33131,69],{},[14,33133,1807],{"id":1806},[18,33135,1810,33136,1813],{},[22,33137,645],{},[144,33139,33140],{"className":197,"code":1816,"language":199,"meta":150,"style":150},[22,33141,33142,33148,33156,33168,33180,33190,33196,33200,33204],{"__ignoreMap":150},[203,33143,33144,33146],{"class":205,"line":206},[203,33145,245],{"class":209},[203,33147,248],{"class":219},[203,33149,33150,33152,33154],{"class":205,"line":251},[203,33151,1829],{"class":219},[203,33153,1832],{"class":215},[203,33155,257],{"class":219},[203,33157,33158,33160,33162,33164,33166],{"class":205,"line":260},[203,33159,263],{"class":219},[203,33161,28],{"class":209},[203,33163,1843],{"class":219},[203,33165,272],{"class":271},[203,33167,275],{"class":219},[203,33169,33170,33172,33174,33176,33178],{"class":205,"line":278},[203,33171,1852],{"class":219},[203,33173,1855],{"class":215},[203,33175,1858],{"class":219},[203,33177,1861],{"class":215},[203,33179,257],{"class":219},[203,33181,33182,33184,33186,33188],{"class":205,"line":285},[203,33183,1488],{"class":209},[203,33185,212],{"class":209},[203,33187,1872],{"class":215},[203,33189,1496],{"class":219},[203,33191,33192,33194],{"class":205,"line":541},[203,33193,1879],{"class":223},[203,33195,1569],{"class":219},[203,33197,33198],{"class":205,"line":549},[203,33199,1512],{"class":219},[203,33201,33202],{"class":205,"line":787},[203,33203,1517],{"class":219},[203,33205,33206],{"class":205,"line":799},[203,33207,288],{"class":219},[18,33209,1896],{},[14,33211,1900],{"id":1899},[473,33213,1904],{"id":1903},[144,33215,33216],{"className":197,"code":1907,"language":199,"meta":150,"style":150},[22,33217,33218,33224,33230,33242],{"__ignoreMap":150},[203,33219,33220,33222],{"class":205,"line":206},[203,33221,245],{"class":209},[203,33223,248],{"class":219},[203,33225,33226,33228],{"class":205,"line":251},[203,33227,1114],{"class":215},[203,33229,257],{"class":219},[203,33231,33232,33234,33236,33238,33240],{"class":205,"line":260},[203,33233,263],{"class":219},[203,33235,28],{"class":209},[203,33237,1930],{"class":219},[203,33239,272],{"class":271},[203,33241,275],{"class":219},[203,33243,33244],{"class":205,"line":278},[203,33245,288],{"class":219},[18,33247,1941],{},[473,33249,1945],{"id":1944},[144,33251,33252],{"className":197,"code":1948,"language":199,"meta":150,"style":150},[22,33253,33254,33264,33272],{"__ignoreMap":150},[203,33255,33256,33258,33260,33262],{"class":205,"line":206},[203,33257,28],{"class":209},[203,33259,1930],{"class":219},[203,33261,272],{"class":271},[203,33263,275],{"class":219},[203,33265,33266,33268,33270],{"class":205,"line":251},[203,33267,455],{"class":209},[203,33269,719],{"class":718},[203,33271,1297],{"class":219},[203,33273,33274],{"class":205,"line":260},[203,33275,288],{"class":219},[18,33277,1975],{},[473,33279,1979],{"id":1978},[18,33281,1982],{},[473,33283,1986],{"id":1985},[18,33285,1989],{},[14,33287,1993],{"id":1992},[299,33289,33290,33298],{},[302,33291,33292],{},[305,33293,33294,33296],{},[308,33295,2002],{},[308,33297,2005],{},[315,33299,33300,33312,33318,33324,33330,33338],{},[305,33301,33302,33308],{},[320,33303,33304,2014,33306,2017],{},[22,33305,39],{},[22,33307,43],{},[320,33309,2020,33310,2023],{},[22,33311,32],{},[305,33313,33314,33316],{},[320,33315,2028],{},[320,33317,2031],{},[305,33319,33320,33322],{},[320,33321,2036],{},[320,33323,2039],{},[305,33325,33326,33328],{},[320,33327,2044],{},[320,33329,2047],{},[305,33331,33332,33336],{},[320,33333,33334,2054],{},[22,33335,1279],{},[320,33337,2057],{},[305,33339,33340,33344],{},[320,33341,1810,33342,2064],{},[22,33343,32],{},[320,33345,2067],{},[14,33347,2071],{"id":2070},[159,33349,33350,33355,33360,33365,33370],{},[162,33351,33352],{},[119,33353,2081],{"href":2078,"rel":33354},[2080],[162,33356,33357],{},[119,33358,2088],{"href":2086,"rel":33359},[2080],[162,33361,33362],{},[119,33363,2095],{"href":2093,"rel":33364},[2080],[162,33366,33367],{},[119,33368,2102],{"href":2100,"rel":33369},[2080],[162,33371,33372],{},[119,33373,2109],{"href":2107,"rel":33374},[2080],[2111,33376,2113],{},{"title":150,"searchDepth":251,"depth":251,"links":33378},[33379,33380,33381,33382,33383,33384,33385,33389,33390,33391,33392,33393,33394,33395,33399,33400,33406,33407],{"id":16,"depth":251,"text":16},{"id":115,"depth":251,"text":115},{"id":132,"depth":251,"text":132},{"id":135,"depth":251,"text":136},{"id":291,"depth":251,"text":292},{"id":381,"depth":251,"text":382},{"id":410,"depth":251,"text":411,"children":33386},[33387,33388],{"id":475,"depth":260,"text":476},{"id":554,"depth":260,"text":555},{"id":674,"depth":251,"text":675},{"id":919,"depth":251,"text":920},{"id":1018,"depth":251,"text":1019},{"id":1092,"depth":251,"text":1093},{"id":1273,"depth":251,"text":1274},{"id":1444,"depth":251,"text":1445},{"id":1644,"depth":251,"text":1645,"children":33396},[33397,33398],{"id":1651,"depth":260,"text":1652},{"id":1721,"depth":260,"text":1722},{"id":1806,"depth":251,"text":1807},{"id":1899,"depth":251,"text":1900,"children":33401},[33402,33403,33404,33405],{"id":1903,"depth":260,"text":1904},{"id":1944,"depth":260,"text":1945},{"id":1978,"depth":260,"text":1979},{"id":1985,"depth":260,"text":1986},{"id":1992,"depth":251,"text":1993},{"id":2070,"depth":251,"text":2071},{},{"title":5,"description":2146},1785748275627]