[{"data":1,"prerenderedAt":9135},["ShallowReactive",2],{"java:mysql-bplus-tree-vs-btree\u002F":3,"java-wiki-navigation":1094},{"id":4,"title":5,"body":6,"commentId":1080,"description":1081,"difficulty":1082,"draft":1083,"extension":1084,"meta":1085,"navigation":1086,"order":1087,"path":1088,"section":1089,"seo":1090,"stem":1091,"updated":1092,"__hash__":1093},"java\u002Fjava\u002Fmysql-bplus-tree-vs-btree.md","MySQL 为何采用 B+ 树？B+ 树与 B 树的区别是什么？",{"type":7,"value":8,"toc":1050},"minimark",[9,13,17,20,36,39,42,46,52,55,69,73,81,101,104,107,121,125,128,131,134,144,147,164,171,175,178,181,187,190,206,209,225,229,340,343,346,350,353,356,359,365,368,375,380,384,387,392,395,402,405,411,414,420,423,427,430,433,439,442,445,448,454,457,463,466,471,480,484,487,514,521,527,530,536,539,542,588,591,595,598,601,607,610,617,621,624,633,636,639,665,668,671,688,691,694,698,701,704,720,723,729,732,741,744,747,751,754,760,763,772,775,795,802,808,811,814,832,835,844,848,851,854,860,863,866,870,873,890,893,897,900,903,909,912,915,929,932,936,940,943,947,950,954,957,961,964,968,981,985,988,994,1000,1005,1008,1046],[10,11,12],"h2",{"id":12},"面试回答",[14,15,16],"p",{},"MySQL InnoDB 的普通索引采用通常所说的 B+ 树结构，核心原因是数据库索引需要同时兼顾磁盘或页访问次数、等值查询、范围查询和顺序遍历。",[14,18,19],{},"B+ 树与 B 树的主要区别是：",[21,22,23,27,30,33],"ul",{},[24,25,26],"li",{},"B 树的非叶子节点和叶子节点都可以保存数据记录；",[24,28,29],{},"B+ 树的非叶子节点主要保存索引键和子页面指针，完整索引记录集中在叶子节点；",[24,31,32],{},"B+ 树的叶子节点按照键值顺序连接，适合范围扫描；",[24,34,35],{},"由于非叶子节点不保存完整数据，同样大小的页面能够容纳更多目录项，树的扇出更大、高度更低。",[14,37,38],{},"InnoDB 默认索引页大小为 16KB。一次读取通常以页为单位，而不是只读取一个键。B+ 树通过较大的扇出，把大量数据组织在较少的层级中，查询通常只需要访问少量页面；定位到范围起点后，还可以沿叶子页顺序扫描，不必反复从根节点查找。",[14,40,41],{},"InnoDB 的聚簇索引叶子节点保存完整行数据，二级索引叶子节点保存二级索引键和主键值。通过二级索引查询非覆盖列时，需要先取得主键，再回到聚簇索引查找完整行，这就是常说的回表。",[43,44,45],"h3",{"id":45},"一句话总结",[47,48,49],"blockquote",{},[14,50,51],{},"B+ 树让非叶子节点专注于导航，以更大的扇出降低树高，并把有序数据集中在相连的叶子节点中，因此既能减少页面访问，又适合范围查询和顺序扫描。",[10,53,54],{"id":54},"详细讲解",[14,56,57],{},[58,59,65],"a",{"href":60,"target":61,"rel":62},"\u002Fimages\u002Fwiki\u002Fjava\u002Fmysql-btree-vs-bplus-tree.svg","_blank",[63,64],"noopener","noreferrer",[66,67],"img",{"src":60,"alt":68},"B 树与 B+ 树结构对比",[10,70,72],{"id":71},"一先明确mysql-文档中的-b-tree-与-b-树","一、先明确：MySQL 文档中的 B-tree 与 B+ 树",[14,74,75,76,80],{},"MySQL 官方文档通常把 InnoDB 的普通索引统称为 ",[77,78,79],"code",{},"B-tree"," 索引。在数据库实现和面试语境中，InnoDB 的这种索引组织通常被称为 B+ 树：",[21,82,83,86,89,92,95,98],{},[24,84,85],{},"索引记录存放在叶子页；",[24,87,88],{},"非叶子页负责索引导航；",[24,90,91],{},"聚簇索引的叶子页保存行数据；",[24,93,94],{},"二级索引的叶子记录保存索引列和主键；",[24,96,97],{},"InnoDB 同一层的索引页通过前后页指针双向连接；",[24,99,100],{},"叶子页中的索引记录有序，能够进行范围扫描。",[14,102,103],{},"因此，“MySQL 使用 B+ 树”通常是在描述 InnoDB 普通索引的具体组织特征；它不代表 MySQL 中所有索引都使用 B+ 树。",[14,105,106],{},"例如：",[21,108,109,112,115,118],{},[24,110,111],{},"InnoDB 普通索引使用 B-tree 类结构；",[24,113,114],{},"空间索引使用 R-tree；",[24,116,117],{},"全文索引有独立的倒排索引设计；",[24,119,120],{},"InnoDB 还可能使用自适应哈希索引加速部分热点等值访问。",[10,122,124],{"id":123},"二b-树是什么","二、B 树是什么",[14,126,127],{},"B 树是一种多路平衡查找树。",[14,129,130],{},"“多路”表示一个节点可以拥有多个子节点，而不是像二叉查找树那样最多只有两个子节点。",[14,132,133],{},"一个简化的 B 树可以表示为：",[135,136,142],"pre",{"className":137,"code":139,"language":140,"meta":141},[138],"language-text","                 [20 | 50]\n                \u002F    |    \\\n       [5 | 10]   [30 | 40]   [60 | 80]\n","text","",[77,143,139],{"__ignoreMap":141},[14,145,146],{},"B 树具有以下特点：",[21,148,149,152,155,158,161],{},[24,150,151],{},"节点中的键有序；",[24,153,154],{},"一个节点可以保存多个键和数据记录；",[24,156,157],{},"非叶子节点也可以直接保存数据；",[24,159,160],{},"所有叶子节点通常处于同一层；",[24,162,163],{},"插入和删除后通过分裂、合并或旋转维持平衡。",[14,165,166,167,170],{},"查询 ",[77,168,169],{},"20"," 时，可能在根节点直接找到数据，不一定需要访问叶子节点。",[10,172,174],{"id":173},"三b-树是什么","三、B+ 树是什么",[14,176,177],{},"B+ 树也是多路平衡查找树，但它把导航结构和数据记录进一步分离。",[14,179,180],{},"一个简化的 B+ 树可以表示为：",[135,182,185],{"className":183,"code":184,"language":140,"meta":141},[138],"                         [20 | 50]\n                        \u002F    |    \\\n                       \u002F     |     \\\n        [5 | 10 | 20] [30 | 40 | 50] [60 | 80]\n                ↔              ↔\n          叶子页通过前后页指针双向连接\n",[77,186,184],{"__ignoreMap":141},[14,188,189],{},"B+ 树具有以下典型特点：",[21,191,192,195,198,201,203],{},[24,193,194],{},"非叶子节点主要保存键和子节点指针；",[24,196,197],{},"完整的数据记录集中在叶子节点；",[24,199,200],{},"所有查询最终都会到达叶子节点；",[24,202,97],{},[24,204,205],{},"适合从某个键开始连续读取一段数据。",[14,207,208],{},"需要注意，示意图中的分隔键可能同时出现在非叶子节点和叶子节点中。非叶子节点中的键用于导航，叶子节点中的索引记录才承载最终数据定位信息。",[14,210,211,212,216,217,220,221,224],{},"这里的双向链表连接的是",[213,214,215],"strong",{},"索引页","，并不是说整棵 B+ 树本身就是双向链表。整体结构仍然是一棵多路平衡树：树结构负责快速定位目标页；同层页面之间的 ",[77,218,219],{},"FIL_PAGE_PREV","、",[77,222,223],{},"FIL_PAGE_NEXT"," 指针负责前后遍历。单个索引页内部的记录则主要按键值顺序组成单向链表，并由页目录辅助查找。",[10,226,228],{"id":227},"四b-树与-b-树的核心区别","四、B 树与 B+ 树的核心区别",[230,231,232,248],"table",{},[233,234,235],"thead",{},[236,237,238,242,245],"tr",{},[239,240,241],"th",{},"对比项",[239,243,244],{},"B 树",[239,246,247],{},"B+ 树",[249,250,251,263,274,285,296,307,318,329],"tbody",{},[236,252,253,257,260],{},[254,255,256],"td",{},"数据记录位置",[254,258,259],{},"非叶子节点和叶子节点都可以保存",[254,261,262],{},"完整索引记录集中在叶子节点",[236,264,265,268,271],{},[254,266,267],{},"非叶子节点职责",[254,269,270],{},"导航，同时可能保存数据",[254,272,273],{},"主要负责导航",[236,275,276,279,282],{},[254,277,278],{},"单个节点可容纳的键",[254,280,281],{},"相对较少",[254,283,284],{},"通常更多",[236,286,287,290,293],{},[254,288,289],{},"树的扇出",[254,291,292],{},"相对较小",[254,294,295],{},"通常更大",[236,297,298,301,304],{},[254,299,300],{},"树高",[254,302,303],{},"相同数据量下可能更高",[254,305,306],{},"相同数据量下通常更低",[236,308,309,312,315],{},[254,310,311],{},"等值查询",[254,313,314],{},"可能在非叶子节点提前结束",[254,316,317],{},"通常需要走到叶子节点",[236,319,320,323,326],{},[254,321,322],{},"范围查询",[254,324,325],{},"可能需要在树中多次定位",[254,327,328],{},"定位起点后可顺序扫描叶子节点",[236,330,331,334,337],{},[254,332,333],{},"查询路径",[254,335,336],{},"不同记录的结束层级可能不同",[254,338,339],{},"通常都到叶子层，路径更稳定",[14,341,342],{},"不能简单得出“B+ 树在所有场景都比 B 树快”的结论。",[14,344,345],{},"如果整个数据结构都在内存中，而且只做等值查询，B 树可能在非叶子节点提前命中。但数据库索引更关心页面访问、范围扫描和批量读取，B+ 树的整体特性更适合这类负载。",[10,347,349],{"id":348},"五为什么数据库关心页面访问次数","五、为什么数据库关心页面访问次数",[14,351,352],{},"InnoDB 管理数据的基本单位是页，默认页面大小为 16KB。",[14,354,355],{},"查询一个索引键时，底层不是只从磁盘读取这个键对应的几个字节，而是把相关页面读入 Buffer Pool，再在页内查找。",[14,357,358],{},"因此，索引设计要尽量做到：",[135,360,363],{"className":361,"code":362,"language":140,"meta":141},[138],"更少的树层级\n        ↓\n更少的页面访问\n        ↓\n更低的 I\u002FO 和缓存查找成本\n",[77,364,362],{"__ignoreMap":141},[14,366,367],{},"即使数据已经在 Buffer Pool 中，较低的树高仍然意味着更少的页面定位、Latch 竞争和 CPU 缓存访问。",[14,369,370,371,374],{},"所以分析 B+ 树时，不应只背诵时间复杂度 ",[77,372,373],{},"O(log N)","，还要关注：",[47,376,377],{},[14,378,379],{},"这个对数以多大的扇出为底，以及一次查询需要访问多少个页面。",[10,381,383],{"id":382},"六扇出是什么","六、扇出是什么",[14,385,386],{},"**扇出（Fan-out）是一个非叶子节点能够直接指向的子节点数量。**在 InnoDB 中，可以把它理解为：",[47,388,389],{},[14,390,391],{},"一个非叶子索引页能够直接导航到多少个下层索引页。",[14,393,394],{},"例如，某个非叶子页保存了大量“索引值 + 子页面指针”目录项，并能够据此导航到约 1,000 个子页面，那么可以近似地说这个节点的扇出约为 1,000。",[14,396,397,398,401],{},"扇出不是整棵树的记录总数，也不是一个叶子页能够保存的行数。它描述的是",[213,399,400],{},"单个非叶子节点向下一层分出的分支数量","。",[14,403,404],{},"对于相同数量的数据，树高可以粗略理解为：",[135,406,409],{"className":407,"code":408,"language":140,"meta":141},[138],"树高 ≈ log(扇出)(需要管理的页面数)\n",[77,410,408],{"__ignoreMap":141},[14,412,413],{},"扇出越大，每增加一层能够覆盖的页面数增长得越快，因此管理相同数据量所需的层级通常越少：",[135,415,418],{"className":416,"code":417,"language":140,"meta":141},[138],"扇出为 2：       2 → 4 → 8 → 16\n扇出为 1,000：   1,000 → 1,000,000 → 1,000,000,000\n",[77,419,417],{"__ignoreMap":141},[14,421,422],{},"这里的数字只用于说明增长速度。真实扇出会受到索引键长度、页面格式、记录头、页面填充率等因素影响。",[10,424,426],{"id":425},"七b-树如何通过更大的扇出降低树高","七、B+ 树如何通过更大的扇出降低树高",[14,428,429],{},"假设一个非叶子页既保存键，又保存完整行数据，那么每个目录项会比较大，一个页面能够容纳的目录项就比较少。",[14,431,432],{},"如果非叶子页只保存：",[135,434,437],{"className":435,"code":436,"language":140,"meta":141},[138],"索引键 + 子页面指针\n",[77,438,436],{"__ignoreMap":141},[14,440,441],{},"每个目录项更小，同一个 16KB 页面就能容纳更多目录项，一个节点可以指向更多子页面。",[14,443,444],{},"由于单个目录项更紧凑，同一个页面可以保存更多目录项、指向更多子页面，这就是 B+ 树扇出更大的原因。",[14,446,447],{},"例如，仅用于理解数量级，假设一个非叶子目录项平均占用 16 字节，忽略页头、槽目录、填充率和其他开销：",[135,449,452],{"className":450,"code":451,"language":140,"meta":141},[138],"16KB ÷ 16B ≈ 1024\n",[77,453,451],{"__ignoreMap":141},[14,455,456],{},"一个非叶子页理论上可以导航约一千个子页面：",[135,458,461],{"className":459,"code":460,"language":140,"meta":141},[138],"根节点：约 1,000 个分支\n第二层：约 1,000 × 1,000 个分支\n第三层：继续扩大\n",[77,462,460],{"__ignoreMap":141},[14,464,465],{},"真实容量会受到键长度、页格式、记录头、填充率等因素影响，不能直接按这个示例计算生产容量。但它能说明：",[47,467,468],{},[14,469,470],{},"多路树的扇出很大，少量层级就可以管理大量记录。",[14,472,473],{},[58,474,477],{"href":475,"target":61,"rel":476},"\u002Fimages\u002Fwiki\u002Fjava\u002Fmysql-bplus-tree-fanout.svg",[63,64],[66,478],{"src":475,"alt":479},"B+ 树通过大扇出降低树高",[10,481,483],{"id":482},"八为什么-b-树适合范围查询","八、为什么 B+ 树适合范围查询",[14,485,486],{},"假设执行：",[135,488,492],{"className":489,"code":490,"language":491,"meta":141,"style":141},"language-sql shiki shiki-themes github-light github-dark","SELECT *\nFROM orders\nWHERE id BETWEEN 1000 AND 2000;\n","sql",[77,493,494,502,508],{"__ignoreMap":141},[495,496,499],"span",{"class":497,"line":498},"line",1,[495,500,501],{},"SELECT *\n",[495,503,505],{"class":497,"line":504},2,[495,506,507],{},"FROM orders\n",[495,509,511],{"class":497,"line":510},3,[495,512,513],{},"WHERE id BETWEEN 1000 AND 2000;\n",[14,515,516,517,520],{},"B+ 树可以先从根节点定位到 ",[77,518,519],{},"id=1000"," 附近的叶子页：",[135,522,525],{"className":523,"code":524,"language":140,"meta":141},[138],"根页\n  ↓\n非叶子页\n  ↓\n包含 1000 的叶子页\n",[77,526,524],{"__ignoreMap":141},[14,528,529],{},"定位范围起点以后，可以沿叶子页顺序扫描：",[135,531,534],{"className":532,"code":533,"language":140,"meta":141},[138],"[1000 ... 1200] ↔ [1201 ... 1500] ↔ [1501 ... 1800] ↔ [1801 ... 2000]\n",[77,535,533],{"__ignoreMap":141},[14,537,538],{},"不需要为范围中的每一条记录重新从根节点查找。",[14,540,541],{},"因此，B+ 树天然适合：",[21,543,544,559,564,570,576,585],{},[24,545,546,220,549,220,552,220,555,558],{},[77,547,548],{},">",[77,550,551],{},">=",[77,553,554],{},"\u003C",[77,556,557],{},"\u003C=","；",[24,560,561,558],{},[77,562,563],{},"BETWEEN",[24,565,566,567,558],{},"有效前缀的 ",[77,568,569],{},"LIKE 'abc%'",[24,571,572,573,558],{},"按索引顺序执行的 ",[77,574,575],{},"ORDER BY",[24,577,578,579,220,582,558],{},"部分 ",[77,580,581],{},"MIN",[77,583,584],{},"MAX",[24,586,587],{},"范围扫描和顺序分页。",[14,589,590],{},"能否真正使用索引顺序，还取决于联合索引结构、最左前缀、排序方向和执行计划，不能因为底层是 B+ 树就认为所有范围条件都会高效。",[10,592,594],{"id":593},"九为什么不用二叉查找树或红黑树","九、为什么不用二叉查找树或红黑树",[14,596,597],{},"二叉查找树和红黑树的扇出通常是 2。",[14,599,600],{},"即使红黑树能够保持近似平衡，管理大量数据时树高仍然明显高于多路树：",[135,602,605],{"className":603,"code":604,"language":140,"meta":141},[138],"二叉树：\n每个节点最多 2 个分支\n\nB+ 树：\n每个页面可以拥有数百甚至更多分支\n",[77,606,604],{"__ignoreMap":141},[14,608,609],{},"如果每层节点可能位于不同页面，树越高，查询可能需要访问的页面越多。",[14,611,612,613,616],{},"红黑树很适合内存中的集合和映射，例如 Java 的 ",[77,614,615],{},"TreeMap","；数据库索引需要尽量减少页面 I\u002FO，并支持大范围顺序扫描，因此多路 B+ 树更加合适。",[10,618,620],{"id":619},"十为什么不直接使用哈希索引","十、为什么不直接使用哈希索引",[14,622,623],{},"哈希结构擅长等值查询：",[135,625,627],{"className":489,"code":626,"language":491,"meta":141,"style":141},"WHERE id = 100\n",[77,628,629],{"__ignoreMap":141},[495,630,631],{"class":497,"line":498},[495,632,626],{},[14,634,635],{},"理想情况下可以根据哈希值直接定位桶。",[14,637,638],{},"但哈希值不保留原始键的顺序，因此不适合：",[135,640,642],{"className":489,"code":641,"language":491,"meta":141,"style":141},"WHERE id > 100;\nWHERE id BETWEEN 100 AND 200;\nORDER BY id;\nLIKE 'abc%';\n",[77,643,644,649,654,659],{"__ignoreMap":141},[495,645,646],{"class":497,"line":498},[495,647,648],{},"WHERE id > 100;\n",[495,650,651],{"class":497,"line":504},[495,652,653],{},"WHERE id BETWEEN 100 AND 200;\n",[495,655,656],{"class":497,"line":510},[495,657,658],{},"ORDER BY id;\n",[495,660,662],{"class":497,"line":661},4,[495,663,664],{},"LIKE 'abc%';\n",[14,666,667],{},"哈希索引还需要处理哈希冲突，也无法自然支持联合索引的有序最左前缀扫描。",[14,669,670],{},"B+ 树虽然等值查找通常需要沿树下降，但同时支持：",[21,672,673,676,679,682,685],{},[24,674,675],{},"等值查询；",[24,677,678],{},"范围查询；",[24,680,681],{},"顺序扫描；",[24,683,684],{},"排序；",[24,686,687],{},"联合索引前缀匹配。",[14,689,690],{},"因此它的通用性更强。",[14,692,693],{},"InnoDB 的自适应哈希索引可以为部分热点 B-tree 页面建立哈希访问路径，但它属于内部优化，不会替代原有 B+ 树索引。",[10,695,697],{"id":696},"十一innodb-聚簇索引如何组织数据","十一、InnoDB 聚簇索引如何组织数据",[14,699,700],{},"每张 InnoDB 表都有一个聚簇索引。",[14,702,703],{},"聚簇索引的选择顺序通常是：",[705,706,707,710,717],"ol",{},[24,708,709],{},"显式定义的主键；",[24,711,712,713,716],{},"第一个所有列均为 ",[77,714,715],{},"NOT NULL"," 的唯一索引；",[24,718,719],{},"InnoDB 自动生成的隐藏行 ID。",[14,721,722],{},"聚簇索引可以简化为：",[135,724,727],{"className":725,"code":726,"language":140,"meta":141},[138],"非叶子节点：\n主键 + 子页面指针\n\n叶子节点：\n主键 + 完整行数据\n",[77,728,726],{"__ignoreMap":141},[14,730,731],{},"因此，通过主键查询：",[135,733,735],{"className":489,"code":734,"language":491,"meta":141,"style":141},"SELECT * FROM users WHERE id = 100;\n",[77,736,737],{"__ignoreMap":141},[495,738,739],{"class":497,"line":498},[495,740,734],{},[14,742,743],{},"沿聚簇索引找到叶子记录时，通常已经取得了完整行数据。",[14,745,746],{},"这也是为什么官方文档把 InnoDB 表称为按聚簇索引组织的数据结构。",[10,748,750],{"id":749},"十二innodb-二级索引如何组织数据","十二、InnoDB 二级索引如何组织数据",[14,752,753],{},"二级索引的叶子记录不直接保存完整行，而是保存：",[135,755,758],{"className":756,"code":757,"language":140,"meta":141},[138],"二级索引列 + 主键值\n",[77,759,757],{"__ignoreMap":141},[14,761,762],{},"假设存在：",[135,764,766],{"className":489,"code":765,"language":491,"meta":141,"style":141},"CREATE INDEX idx_users_name ON users(name);\n",[77,767,768],{"__ignoreMap":141},[495,769,770],{"class":497,"line":498},[495,771,765],{},[14,773,774],{},"执行：",[135,776,778],{"className":489,"code":777,"language":491,"meta":141,"style":141},"SELECT age\nFROM users\nWHERE name = 'Tom';\n",[77,779,780,785,790],{"__ignoreMap":141},[495,781,782],{"class":497,"line":498},[495,783,784],{},"SELECT age\n",[495,786,787],{"class":497,"line":504},[495,788,789],{},"FROM users\n",[495,791,792],{"class":497,"line":510},[495,793,794],{},"WHERE name = 'Tom';\n",[14,796,797,798,801],{},"如果 ",[77,799,800],{},"age"," 不在二级索引中，查询过程通常是：",[135,803,806],{"className":804,"code":805,"language":140,"meta":141},[138],"idx_users_name B+ 树\n        ↓\n找到 name='Tom' 对应的主键\n        ↓\n回到聚簇索引 B+ 树\n        ↓\n根据主键取得完整行和 age\n",[77,807,805],{"__ignoreMap":141},[14,809,810],{},"这就是回表。",[14,812,813],{},"如果查询所需列都能从二级索引叶子记录中取得：",[135,815,817],{"className":489,"code":816,"language":491,"meta":141,"style":141},"SELECT id\nFROM users\nWHERE name = 'Tom';\n",[77,818,819,824,828],{"__ignoreMap":141},[495,820,821],{"class":497,"line":498},[495,822,823],{},"SELECT id\n",[495,825,826],{"class":497,"line":504},[495,827,789],{},[495,829,830],{"class":497,"line":510},[495,831,794],{},[14,833,834],{},"那么可能直接通过二级索引完成查询，不需要回到聚簇索引，这就是覆盖索引。",[14,836,837],{},[58,838,841],{"href":839,"target":61,"rel":840},"\u002Fimages\u002Fwiki\u002Fjava\u002Fmysql-innodb-index-lookup.svg",[63,64],[66,842],{"src":839,"alt":843},"InnoDB 聚簇索引、二级索引与回表",[10,845,847],{"id":846},"十三为什么主键不宜过长","十三、为什么主键不宜过长",[14,849,850],{},"InnoDB 的每条二级索引记录都包含主键值。",[14,852,853],{},"如果主键很长：",[135,855,858],{"className":856,"code":857,"language":140,"meta":141},[138],"每条二级索引记录更大\n        ↓\n一个索引页容纳的记录更少\n        ↓\n索引占用空间增加\n        ↓\n缓存命中率和扇出可能下降\n",[77,859,857],{"__ignoreMap":141},[14,861,862],{},"因此，在满足业务要求的前提下，较短且稳定的主键通常更有利于 InnoDB 索引组织。",[14,864,865],{},"这并不意味着所有表都必须使用自增整数主键。是否采用自增 ID、分布式 ID 或业务主键，还要考虑写入热点、分库分表、数据合并和业务语义。",[10,867,869],{"id":868},"十四叶子节点有序带来的其他收益","十四、叶子节点有序带来的其他收益",[14,871,872],{},"除了范围查询，叶子节点按键值有序还可以帮助：",[21,874,875,878,881,884,887],{},[24,876,877],{},"按索引顺序输出数据，减少额外排序；",[24,879,880],{},"快速定位最小值和最大值；",[24,882,883],{},"连续读取相邻索引记录；",[24,885,886],{},"对联合索引执行最左前缀扫描；",[24,888,889],{},"利用页预读和 Buffer Pool 提高连续访问效率。",[14,891,892],{},"但“逻辑有序”不等于“所有叶子页在磁盘上永远物理连续”。页面分裂、删除、合并和长期更新都会产生碎片，页之间主要通过页号和链路维持逻辑顺序。",[10,894,896],{"id":895},"十五b-树的写入代价","十五、B+ 树的写入代价",[14,898,899],{},"B+ 树并不是只有优点。",[14,901,902],{},"插入新记录时，需要先找到目标叶子页。如果页面空间不足，可能发生页分裂：",[135,904,907],{"className":905,"code":906,"language":140,"meta":141},[138],"一个满页\n   ↓\n分裂为两个页\n   ↓\n更新父节点目录项\n",[77,908,906],{"__ignoreMap":141},[14,910,911],{},"删除大量记录后，也可能触发页面合并或树结构收缩。",[14,913,914],{},"随机主键写入容易把数据分散到不同叶子页，可能带来：",[21,916,917,920,923,926],{},[24,918,919],{},"更多随机页面访问；",[24,921,922],{},"页分裂；",[24,924,925],{},"页面利用率下降；",[24,927,928],{},"Buffer Pool 压力增加。",[14,930,931],{},"顺序递增键通常更容易写入索引右侧页面，但高并发场景下也可能形成右侧热点。主键选择需要结合实际写入模式评估。",[10,933,935],{"id":934},"十六常见误区","十六、常见误区",[43,937,939],{"id":938},"误区-1mysql-所有索引都是-b-树","误区 1：MySQL 所有索引都是 B+ 树",[14,941,942],{},"不准确。这里主要讨论 InnoDB 的普通索引。空间索引、全文索引和内部自适应哈希索引采用不同结构或机制。",[43,944,946],{"id":945},"误区-2b-树的所有数据都只存在叶子节点","误区 2：B+ 树的所有数据都只存在叶子节点",[14,948,949],{},"需要区分“完整索引记录”和“分隔键”。非叶子节点也会保存用于导航的键，但完整索引记录集中在叶子节点。",[43,951,953],{"id":952},"误区-3b-树一定比-b-树快","误区 3：B+ 树一定比 B 树快",[14,955,956],{},"不一定。纯内存、单次等值查询中，B 树可能提前在非叶子节点命中。B+ 树的优势主要体现在页面扇出、稳定查询路径和范围扫描。",[43,958,960],{"id":959},"误区-4使用-b-树后查询一定只需要三次-io","误区 4：使用 B+ 树后查询一定只需要三次 I\u002FO",[14,962,963],{},"树高不是固定值，取决于页面大小、键长度、记录大小、填充率和数据量。页面还可能已经位于 Buffer Pool 中，因此树层访问次数也不能直接等同于磁盘 I\u002FO 次数。",[43,965,967],{"id":966},"误区-5建立索引就一定能提高性能","误区 5：建立索引就一定能提高性能",[14,969,970,971,220,974,220,977,980],{},"索引会占用空间并增加 ",[77,972,973],{},"INSERT",[77,975,976],{},"UPDATE",[77,978,979],{},"DELETE"," 的维护成本。低选择性索引、无法满足最左前缀的查询或返回大量数据的查询，也可能不使用索引。",[10,982,984],{"id":983},"十七回答时可以怎样组织","十七、回答时可以怎样组织",[14,986,987],{},"面试时可以按照以下顺序回答：",[135,989,992],{"className":990,"code":991,"language":140,"meta":141},[138],"先说结构差异\n    ↓\n再说数据库按页读取\n    ↓\n解释扇出更大、树高更低\n    ↓\n解释叶子节点有序，适合范围扫描\n    ↓\n结合 InnoDB 聚簇索引和二级索引\n",[77,993,991],{"__ignoreMap":141},[14,995,996,997,999],{},"重点不是只说“B+ 树查询复杂度是 ",[77,998,373],{},"”，而是说明：",[47,1001,1002],{},[14,1003,1004],{},"InnoDB 通过页组织索引，B+ 树让一个非叶子页保存更多导航项，从而用较少层级管理大量数据；定位到叶子页后，又能沿有序叶子页完成范围扫描。",[10,1006,1007],{"id":1007},"参考资料",[21,1009,1010,1018,1025,1032,1039],{},[24,1011,1012],{},[58,1013,1017],{"href":1014,"rel":1015},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-index-types.html",[1016],"nofollow","MySQL 8.4 Reference Manual：Clustered and Secondary Indexes",[24,1019,1020],{},[58,1021,1024],{"href":1022,"rel":1023},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-physical-structure.html",[1016],"MySQL 8.4 Reference Manual：The Physical Structure of an InnoDB Index",[24,1026,1027],{},[58,1028,1031],{"href":1029,"rel":1030},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Fcolumn-indexes.html",[1016],"MySQL 8.4 Reference Manual：Column Indexes",[24,1033,1034],{},[58,1035,1038],{"href":1036,"rel":1037},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Findex-btree-hash.html",[1016],"MySQL 8.4 Reference Manual：Comparison of B-Tree and Hash Indexes",[24,1040,1041],{},[58,1042,1045],{"href":1043,"rel":1044},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-file-space.html",[1016],"MySQL 8.4 Reference Manual：File Space Management",[1047,1048,1049],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}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":141,"searchDepth":504,"depth":504,"links":1051},[1052,1055,1056,1057,1058,1059,1060,1061,1062,1063,1064,1065,1066,1067,1068,1069,1070,1071,1078,1079],{"id":12,"depth":504,"text":12,"children":1053},[1054],{"id":45,"depth":510,"text":45},{"id":54,"depth":504,"text":54},{"id":71,"depth":504,"text":72},{"id":123,"depth":504,"text":124},{"id":173,"depth":504,"text":174},{"id":227,"depth":504,"text":228},{"id":348,"depth":504,"text":349},{"id":382,"depth":504,"text":383},{"id":425,"depth":504,"text":426},{"id":482,"depth":504,"text":483},{"id":593,"depth":504,"text":594},{"id":619,"depth":504,"text":620},{"id":696,"depth":504,"text":697},{"id":749,"depth":504,"text":750},{"id":846,"depth":504,"text":847},{"id":868,"depth":504,"text":869},{"id":895,"depth":504,"text":896},{"id":934,"depth":504,"text":935,"children":1072},[1073,1074,1075,1076,1077],{"id":938,"depth":510,"text":939},{"id":945,"depth":510,"text":946},{"id":952,"depth":510,"text":953},{"id":959,"depth":510,"text":960},{"id":966,"depth":510,"text":967},{"id":983,"depth":504,"text":984},{"id":1007,"depth":504,"text":1007},"wiki:java:mysql-bplus-tree-vs-btree","从磁盘页、树高、扇出、范围查询和 InnoDB 索引组织方式出发，解释 MySQL 为何采用 B+ 树，并对比 B 树、红黑树与哈希索引。","advanced",false,"md",{},true,76,"\u002Fjava\u002Fmysql-bplus-tree-vs-btree","面试专题",{"title":5,"description":1081},"java\u002Fmysql-bplus-tree-vs-btree","2026-07-25","GBUeRH40EIWqqKIkmm3guLyrwBIyYsEbg_LMTTmzgLI",[1095,2522,3428,4614,5628,6969,8391],{"id":1096,"title":1097,"body":1098,"commentId":2512,"description":2513,"difficulty":1082,"draft":1083,"extension":1084,"meta":2514,"navigation":1086,"order":2515,"path":2516,"section":2517,"seo":2518,"stem":2519,"updated":2520,"__hash__":2521},"java\u002Fjava\u002Fjdk8-synchronized-lock-states.md","JDK 8 synchronized 设计与锁状态转换",{"type":7,"value":1099,"toc":2456},[1100,1104,1110,1121,1124,1130,1133,1142,1147,1150,1155,1161,1165,1168,1174,1181,1184,1190,1193,1196,1244,1248,1251,1257,1263,1266,1275,1280,1284,1291,1305,1419,1425,1431,1434,1437,1443,1446,1450,1456,1459,1465,1472,1475,1507,1511,1514,1517,1520,1524,1527,1530,1537,1540,1545,1549,1552,1558,1561,1564,1567,1573,1576,1579,1582,1588,1591,1597,1603,1607,1614,1617,1623,1626,1637,1640,1643,1646,1652,1655,1658,1661,1665,1668,1671,1675,1678,1682,1685,1689,1692,1695,1700,1704,1707,1709,1712,1715,1721,1724,1727,1730,1734,1748,1751,1757,1760,1766,1769,1775,1778,1782,1785,1789,1792,1795,1798,1804,1810,1814,1817,1823,1826,1832,1835,1838,1842,1845,1859,1862,1865,1871,1874,1880,1884,1891,1897,1900,1906,1909,1912,1926,1929,1933,1938,1944,1950,1960,1965,1969,1972,1975,1986,1989,1992,1995,1999,2175,2179,2183,2189,2193,2196,2202,2205,2209,2215,2219,2222,2255,2258,2309,2312,2338,2341,2347,2350,2356,2360,2364,2367,2371,2374,2378,2381,2385,2388,2392,2395,2399,2402,2406,2443,2446,2454],[10,1101,1103],{"id":1102},"一先给出整体结论","一、先给出整体结论",[14,1105,1106,1109],{},[77,1107,1108],{},"synchronized"," 是 Java 语言提供的内置同步机制。它在语义上负责三件事：",[21,1111,1112,1115,1118],{},[24,1113,1114],{},"同一时刻只允许满足条件的线程进入临界区；",[24,1116,1117],{},"支持同一线程重复获取同一把锁，也就是可重入；",[24,1119,1120],{},"解锁 happens-before 后续对同一 Monitor 的加锁，保证相关共享数据的可见性和有序性。",[14,1122,1123],{},"在 JDK 8 HotSpot 中，为了避免所有同步操作都直接阻塞线程，JVM 会根据竞争情况使用不同实现：",[135,1125,1128],{"className":1126,"code":1127,"language":140,"meta":141},[138],"偏向锁：同一个线程反复进入，尽量避免原子操作\n轻量级锁：线程交替执行或短暂竞争，使用栈上 Lock Record 和 CAS\n重量级锁：竞争较强或必须使用 Monitor 能力时，通过 ObjectMonitor 管理等待线程\n",[77,1129,1127],{"__ignoreMap":141},[14,1131,1132],{},"通常所说的“无锁、偏向锁、轻量级锁、重量级锁”，是对对象头及其关联同步结构的概括，不是 Java 语言规范定义的四种锁类型。",[14,1134,1135],{},[58,1136,1139],{"href":1137,"target":61,"rel":1138},"\u002Fimages\u002Fwiki\u002Fjava\u002Fjdk8-synchronized-state-transitions.svg",[63,64],[66,1140],{"src":1137,"alt":1141},"JDK 8 synchronized 锁状态转换总览",[47,1143,1144],{},[14,1145,1146],{},"图中信息较多，可点击图片查看原图。",[14,1148,1149],{},"需要先纠正一个常见说法：",[47,1151,1152],{},[14,1153,1154],{},"“锁只能升级，不能降级”适合帮助理解竞争路径，但并不严谨。",[14,1156,1157,1158],{},"轻量级锁正常释放后会通过 CAS 恢复对象原来的 Mark Word；已经膨胀的 ObjectMonitor 也可能在后续安全点被 JVM 回收。更准确的说法是：",[213,1159,1160],{},"持锁竞争过程中通常不会为了当前请求立即从重量级锁退回轻量级锁，但对象状态并非终身只能单向变化。",[10,1162,1164],{"id":1163},"二synchronized-在字节码中的入口","二、synchronized 在字节码中的入口",[14,1166,1167],{},"同步代码块通常编译为：",[135,1169,1172],{"className":1170,"code":1171,"language":140,"meta":141},[138],"monitorenter\n   临界区代码\nmonitorexit\n",[77,1173,1171],{"__ignoreMap":141},[14,1175,1176,1177,1180],{},"编译器会为正常返回和异常退出路径生成对应的 ",[77,1178,1179],{},"monitorexit","，保证退出同步块时释放锁。",[14,1182,1183],{},"同步方法不会在方法体中出现这两个指令，而是在方法访问标志中使用：",[135,1185,1188],{"className":1186,"code":1187,"language":140,"meta":141},[138],"ACC_SYNCHRONIZED\n",[77,1189,1187],{"__ignoreMap":141},[14,1191,1192],{},"JVM 在调用和返回同步方法时完成 Monitor 的进入与退出。",[14,1194,1195],{},"无论入口是哪一种，最终都围绕同一个锁对象工作：",[230,1197,1198,1208],{},[233,1199,1200],{},[236,1201,1202,1205],{},[239,1203,1204],{},"写法",[239,1206,1207],{},"实际锁对象",[249,1209,1210,1221,1233],{},[236,1211,1212,1215],{},[254,1213,1214],{},"实例同步方法",[254,1216,1217,1218],{},"当前实例 ",[77,1219,1220],{},"this",[236,1222,1223,1226],{},[254,1224,1225],{},"静态同步方法",[254,1227,1228,1229,1232],{},"当前类对应的 ",[77,1230,1231],{},"Class"," 对象",[236,1234,1235,1238],{},[254,1236,1237],{},"同步代码块",[254,1239,1240,1243],{},[77,1241,1242],{},"synchronized (...)"," 中指定的对象",[10,1245,1247],{"id":1246},"三对象头与-mark-word","三、对象头与 Mark Word",[14,1249,1250],{},"普通 Java 对象的对象头主要包含：",[135,1252,1255],{"className":1253,"code":1254,"language":140,"meta":141},[138],"Mark Word\nKlass Pointer\n",[77,1256,1254],{"__ignoreMap":141},[14,1258,1259,1260,1262],{},"数组对象还会额外保存数组长度。",[77,1261,1108],{}," 的关键状态主要编码在 Mark Word 中。",[14,1264,1265],{},"以 JDK 8、64 位 HotSpot 为例，Mark Word 会复用同一段空间：",[14,1267,1268],{},[58,1269,1272],{"href":1270,"target":61,"rel":1271},"\u002Fimages\u002Fwiki\u002Fjava\u002Fjdk8-synchronized-mark-word.svg",[63,64],[66,1273],{"src":1270,"alt":1274},"JDK 8 HotSpot Mark Word 与同步结构",[47,1276,1277],{},[14,1278,1279],{},"可点击图片查看完整尺寸。",[43,1281,1283],{"id":1282},"统一按最低-3-位阅读","统一按最低 3 位阅读",[14,1285,1286,1287,1290],{},"为了避免一会儿看 2 位、一会儿看 3 位，本文后续统一展示 Mark Word 的最低 ",[213,1288,1289],{},"3 位","。但要先区分“展示宽度”和“字段含义”：",[21,1292,1293,1299,1302],{},[24,1294,1295,1296,558],{},"真正的锁标志始终是最低 ",[213,1297,1298],{},"2 位",[24,1300,1301],{},"只有普通未锁定和偏向布局会把倒数第 3 位解释成“偏向位”；",[24,1303,1304],{},"轻量级锁、重量级锁保存的是对齐后的指针，本文把其最低 3 位也完整写出，但倒数第 3 位不再表示偏向。",[230,1306,1307,1323],{},[233,1308,1309],{},[236,1310,1311,1314,1317,1320],{},[239,1312,1313],{},"最低 3 位",[239,1315,1316],{},"最低 2 位锁标志",[239,1318,1319],{},"状态",[239,1321,1322],{},"倒数第 3 位如何解释",[249,1324,1325,1346,1365,1383,1401],{},[236,1326,1327,1332,1337,1340],{},[254,1328,1329],{},[77,1330,1331],{},"001",[254,1333,1334],{},[77,1335,1336],{},"01",[254,1338,1339],{},"普通未锁定",[254,1341,1342,1343],{},"偏向位为 ",[77,1344,1345],{},"0",[236,1347,1348,1353,1357,1360],{},[254,1349,1350],{},[77,1351,1352],{},"101",[254,1354,1355],{},[77,1356,1336],{},[254,1358,1359],{},"可偏向或已偏向",[254,1361,1342,1362],{},[77,1363,1364],{},"1",[236,1366,1367,1372,1377,1380],{},[254,1368,1369],{},[77,1370,1371],{},"000",[254,1373,1374],{},[77,1375,1376],{},"00",[254,1378,1379],{},"轻量级锁",[254,1381,1382],{},"对齐后的 Lock Record 指针低位，不是偏向位",[236,1384,1385,1390,1395,1398],{},[254,1386,1387],{},[77,1388,1389],{},"010",[254,1391,1392],{},[77,1393,1394],{},"10",[254,1396,1397],{},"重量级锁",[254,1399,1400],{},"对齐后的 ObjectMonitor 指针低位，不是偏向位",[236,1402,1403,1408,1413,1416],{},[254,1404,1405],{},[77,1406,1407],{},"011",[254,1409,1410],{},[77,1411,1412],{},"11",[254,1414,1415],{},"GC 标记",[254,1417,1418],{},"标记状态的一部分，不是偏向位",[14,1420,1421,1422,1424],{},"因此 ",[77,1423,1352],{}," 应当拆开读成：",[135,1426,1429],{"className":1427,"code":1428,"language":140,"meta":141},[138],"偏向位 1 ｜ 锁标志 01\n",[77,1430,1428],{"__ignoreMap":141},[14,1432,1433],{},"它不是一个三位的“锁标志”。本文使用三位只是为了让所有状态在图表中对齐、便于比较。",[14,1435,1436],{},"其中偏向状态可以继续区分：",[135,1438,1441],{"className":1439,"code":1440,"language":140,"meta":141},[138],"匿名偏向：线程指针为空，允许第一个线程认领\n已偏向：线程指针指向获得偏向的 JavaThread\n",[77,1442,1440],{"__ignoreMap":141},[14,1444,1445],{},"Mark Word 是一块被复用的空间。例如普通未锁定对象可以直接保存 identity hash；偏向对象需要保存线程指针，因此二者不能同时使用同一布局。",[43,1447,1449],{"id":1448},"epoch-到底是什么","epoch 到底是什么",[14,1451,1452,1455],{},[77,1453,1454],{},"epoch"," 可以理解为某个类的“偏向版本号”，它不是时间戳，也不是对象年龄。",[14,1457,1458],{},"偏向锁不只依赖对象自身。支持偏向的类会在类的 prototype header 中保存一个当前 epoch；每个偏向对象的 Mark Word 中也会记录对象认领时使用的 epoch。线程进入偏向对象时，会比较两者：",[135,1460,1463],{"className":1461,"code":1462,"language":140,"meta":141},[138],"对象 epoch == 类当前 epoch\n    → 这份偏向记录仍属于当前版本，继续检查偏向线程\n\n对象 epoch != 类当前 epoch\n    → 这份偏向记录已经过期，可以进入重偏向或撤销流程\n",[77,1464,1462],{"__ignoreMap":141},[14,1466,1467,1468,1471],{},"它主要服务于",[213,1469,1470],{},"批量重偏向","。当一批对象从线程 A 整体转交给线程 B 使用时，JVM 不必立即逐个改写所有对象头，而是先提升类的 epoch。旧对象之后被访问时，会因为 epoch 不匹配而按新版本重新处理。",[14,1473,1474],{},"所以需要把两个容易混淆的字段分开：",[230,1476,1477,1487],{},[233,1478,1479],{},[236,1480,1481,1484],{},[239,1482,1483],{},"字段",[239,1485,1486],{},"含义",[249,1488,1489,1498],{},[236,1490,1491,1495],{},[254,1492,1493],{},[77,1494,800],{},[254,1496,1497],{},"对象的 GC 年龄，用于分代回收",[236,1499,1500,1504],{},[254,1501,1502],{},[77,1503,1454],{},[254,1505,1506],{},"类的偏向版本，用于判断对象上的偏向记录是否过期",[10,1508,1510],{"id":1509},"四jdk-8-为什么需要三种锁实现","四、JDK 8 为什么需要三种锁实现",[14,1512,1513],{},"不同竞争场景适合不同成本的同步方式。",[43,1515,1516],{"id":1516},"只有一个线程反复进入",[14,1518,1519],{},"如果一个对象长期只被同一个线程加锁，每次都执行 CAS 仍然是额外成本。偏向锁把对象与线程关联起来，后续同一线程进入时主要检查对象头是否仍偏向自己。",[43,1521,1523],{"id":1522},"多线程交替执行但没有同时竞争","多线程交替执行，但没有同时竞争",[14,1525,1526],{},"例如线程 A 使用完后线程 B 才进入。此时没有必要挂起线程，可以通过栈上 Lock Record 和 CAS 完成加锁、解锁。",[43,1528,1529],{"id":1529},"多线程同时争抢或需要等待队列",[14,1531,1532,1533,1536],{},"当 CAS 快速路径无法解决竞争，或者调用 ",[77,1534,1535],{},"wait()"," 需要条件等待集合时，JVM 使用 ObjectMonitor 管理 Owner、竞争线程和等待线程。",[14,1538,1539],{},"所以这三种实现的目标不是简单比较“谁更高级”，而是：",[47,1541,1542],{},[14,1543,1544],{},"根据实际竞争强度，在原子操作、CPU 自旋、线程阻塞和唤醒成本之间做权衡。",[10,1546,1548],{"id":1547},"五对象初始状态普通未锁定还是匿名偏向","五、对象初始状态：普通未锁定还是匿名偏向",[14,1550,1551],{},"JDK 8 默认启用偏向锁，但默认存在启动延迟：",[135,1553,1556],{"className":1554,"code":1555,"language":140,"meta":141},[138],"-XX:+UseBiasedLocking\n-XX:BiasedLockingStartupDelay=4000\n",[77,1557,1555],{"__ignoreMap":141},[14,1559,1560],{},"因此需要区分对象创建时机：",[43,1562,1563],{"id":1563},"偏向锁尚未启用或被关闭",[14,1565,1566],{},"新对象通常使用普通未锁定布局：",[135,1568,1571],{"className":1569,"code":1570,"language":140,"meta":141},[138],"[identity hash | age | 0 | 01]\n",[77,1572,1570],{"__ignoreMap":141},[14,1574,1575],{},"线程第一次进入同步块时，直接尝试建立轻量级锁。",[43,1577,1578],{"id":1578},"偏向锁已经启用",[14,1580,1581],{},"支持偏向的类创建新对象时，对象通常处于匿名偏向状态：",[135,1583,1586],{"className":1584,"code":1585,"language":140,"meta":141},[138],"[0 | epoch | age | 1 | 01]\n",[77,1587,1585],{"__ignoreMap":141},[14,1589,1590],{},"因此严格来说，不应把这条路径描述为：",[135,1592,1595],{"className":1593,"code":1594,"language":140,"meta":141},[138],"新对象先是无锁，然后第一次进入才“升级”为可偏向状态\n",[77,1596,1594],{"__ignoreMap":141},[14,1598,1599,1600],{},"更准确的说法是：",[213,1601,1602],{},"对象创建时便根据类的 prototype header 获得普通未锁定或匿名偏向的 Mark Word。",[10,1604,1606],{"id":1605},"六偏向锁的获取与重入","六、偏向锁的获取与重入",[14,1608,1609,1610,1613],{},"线程进入一个匿名偏向对象时，会尝试通过 CAS 把自己的 ",[77,1611,1612],{},"JavaThread*","、类当前的 epoch 等信息写入 Mark Word。",[14,1615,1616],{},"成功后，对象变成：",[135,1618,1621],{"className":1619,"code":1620,"language":140,"meta":141},[138],"[当前 JavaThread* | epoch | age | 1 | 01]\n",[77,1622,1620],{"__ignoreMap":141},[14,1624,1625],{},"同一个线程以后再次进入时，主要检查：",[705,1627,1628,1631,1634],{},[24,1629,1630],{},"Mark Word 是否仍是偏向模式；",[24,1632,1633],{},"偏向线程是否是当前线程；",[24,1635,1636],{},"对象 epoch 是否仍与类的 prototype header 匹配。",[14,1638,1639],{},"全部满足时可直接进入，不需要再次通过 CAS 抢占锁。",[43,1641,1642],{"id":1642},"偏向锁退出为什么不清除线程信息",[14,1644,1645],{},"偏向锁针对的就是“同一个线程还会再次进入”这一场景。同步块结束后，Mark Word 通常仍保留对原线程的偏向：",[135,1647,1650],{"className":1648,"code":1649,"language":140,"meta":141},[138],"进入前：偏向线程 A\n执行中：偏向线程 A\n退出后：仍偏向线程 A\n",[77,1651,1649],{"__ignoreMap":141},[14,1653,1654],{},"它不是表示线程 A 永远占用临界区，而是表示下一次线程 A 再进入时可以走低成本路径。",[43,1656,1657],{"id":1657},"偏向锁如何实现可重入",[14,1659,1660],{},"对象头记录了偏向线程，但不通过一个公共计数器记录每次进入。JVM 可以结合当前线程栈上的锁记录识别同步嵌套。对同一偏向线程而言，重复进入不需要竞争性地修改对象头。",[10,1662,1664],{"id":1663},"七其他线程访问偏向对象时发生什么","七、其他线程访问偏向对象时发生什么",[14,1666,1667],{},"线程 B 遇到偏向线程 A 的对象时，不能简单地直接覆盖线程指针。JVM 需要先判断原偏向是否仍然有效。",[14,1669,1670],{},"可能出现以下路径。",[43,1672,1674],{"id":1673},"_1-epoch-已过期","1. epoch 已过期",[14,1676,1677],{},"类发生批量重偏向后，旧对象的 epoch 可能落后于类的 epoch。线程 B 可以尝试通过 CAS 将对象重新偏向自己。",[43,1679,1681],{"id":1680},"_2-原线程已经不再持有这把锁","2. 原线程已经不再持有这把锁",[14,1683,1684],{},"JVM 可以撤销原偏向，使对象恢复为普通未锁定状态，或者在允许的情况下重新偏向新线程。",[43,1686,1688],{"id":1687},"_3-原线程仍在同步块中","3. 原线程仍在同步块中",[14,1690,1691],{},"JVM 需要检查原线程栈，撤销偏向并重建合法的锁状态。如果此时存在实际竞争，后续会进入轻量级竞争或直接膨胀为 ObjectMonitor。",[14,1693,1694],{},"因此：",[47,1696,1697],{},[14,1698,1699],{},"另一个线程访问偏向对象，不等于必然直接升级为重量级锁。是否重偏向、撤销为普通状态、转换为轻量级锁或膨胀，需要结合 epoch、类级启发式统计和原线程是否仍持锁判断。",[10,1701,1703],{"id":1702},"八批量重偏向与批量撤销","八、批量重偏向与批量撤销",[14,1705,1706],{},"如果某个类的大量对象不断被不同线程使用，逐个在安全点撤销偏向的成本会很高。HotSpot 会对同一类的偏向撤销次数做启发式统计。",[43,1708,1470],{"id":1470},[14,1710,1711],{},"当撤销达到一定阈值时，JVM 可以提升类 prototype header 中的 epoch。旧对象不需要立刻逐个修改；之后线程发现对象 epoch 过期时，再尝试将它偏向当前线程。",[14,1713,1714],{},"适合这种模式：",[135,1716,1719],{"className":1717,"code":1718,"language":140,"meta":141},[138],"一批对象先由线程 A 使用\n之后整体交给线程 B 使用\n但同一时刻竞争并不强\n",[77,1720,1718],{"__ignoreMap":141},[43,1722,1723],{"id":1723},"批量撤销",[14,1725,1726],{},"如果该类持续表现出多线程竞争，偏向锁已经不再适合，JVM 可以撤销该类的偏向能力。之后新对象不再默认可偏向，已有对象也会按正常锁路径处理。",[14,1728,1729],{},"这也是为什么偏向锁不能只按“对象自身的四级升级”理解：它还存在以类为粒度的 prototype header、epoch 和批量启发式机制。",[10,1731,1733],{"id":1732},"九普通未锁定到轻量级锁","九、普通未锁定到轻量级锁",[14,1735,1736,1737,1740,1741,1744,1745,1747],{},"当对象处于普通未锁定状态，线程进入同步块时会在当前栈帧创建锁记录，HotSpot 源码中对应 ",[77,1738,1739],{},"BasicLock","，解释器栈中通常由 ",[77,1742,1743],{},"BasicObjectLock"," 将对象引用和 ",[77,1746,1739],{}," 组织在一起。",[14,1749,1750],{},"概念流程如下：",[135,1752,1755],{"className":1753,"code":1754,"language":140,"meta":141},[138],"1. 在线程栈创建 Lock Record\n2. 将对象原 Mark Word 保存到 displaced header\n3. CAS 修改对象 Mark Word\n4. 让 Mark Word 指向当前 Lock Record\n",[77,1756,1754],{"__ignoreMap":141},[14,1758,1759],{},"CAS 成功后：",[135,1761,1764],{"className":1762,"code":1763,"language":140,"meta":141},[138],"对象 Mark Word  →  线程栈 Lock Record\nLock Record     →  保存原 Mark Word\n",[77,1765,1763],{"__ignoreMap":141},[14,1767,1768],{},"对象头低两位为：",[135,1770,1773],{"className":1771,"code":1772,"language":140,"meta":141},[138],"00\n",[77,1774,1772],{"__ignoreMap":141},[14,1776,1777],{},"这就是通常所说的轻量级锁或栈锁。",[43,1779,1781],{"id":1780},"为什么要保存-displaced-header","为什么要保存 displaced header",[14,1783,1784],{},"轻量级锁占用了对象原来的 Mark Word。解锁时需要把原 Mark Word 恢复回对象头，因此先把它保存到 Lock Record。",[10,1786,1788],{"id":1787},"十轻量级锁的可重入","十、轻量级锁的可重入",[14,1790,1791],{},"同一个线程再次获取已经由自己栈锁定的对象时，JVM 会识别对象头中的 Lock Record 指针属于当前线程栈。",[14,1793,1794],{},"此时新的 Lock Record 会使用特殊值表示递归进入，例如 displaced header 置空，而不是再次覆盖对象头。",[14,1796,1797],{},"退出时：",[135,1799,1802],{"className":1800,"code":1801,"language":140,"meta":141},[138],"递归层 Lock Record：只退出当前递归层\n最外层 Lock Record：负责真正恢复对象头\n",[77,1803,1801],{"__ignoreMap":141},[14,1805,1806,1807,1809],{},"这解释了为什么 ",[77,1808,1108],{}," 天然支持可重入。",[10,1811,1813],{"id":1812},"十一轻量级锁如何释放","十一、轻量级锁如何释放",[14,1815,1816],{},"最外层同步块退出时，线程使用 CAS 尝试把 Lock Record 中保存的 displaced header 恢复到对象 Mark Word：",[135,1818,1821],{"className":1819,"code":1820,"language":140,"meta":141},[138],"期望值：对象头仍指向当前 Lock Record\n新值：进入同步块前保存的原 Mark Word\n",[77,1822,1820],{"__ignoreMap":141},[14,1824,1825],{},"CAS 成功：",[135,1827,1830],{"className":1828,"code":1829,"language":140,"meta":141},[138],"轻量级锁正常释放\n对象恢复普通未锁定状态\n",[77,1831,1829],{"__ignoreMap":141},[14,1833,1834],{},"CAS 失败通常意味着对象已在竞争过程中膨胀，不能再直接把原对象头覆盖回去，需要走 ObjectMonitor 的退出逻辑。",[14,1836,1837],{},"这就是“锁只能升级不能降级”表述不够准确的直接例子：没有发生膨胀的轻量级锁，退出后会恢复原 Mark Word。",[10,1839,1841],{"id":1840},"十二轻量级锁何时膨胀","十二、轻量级锁何时膨胀",[14,1843,1844],{},"线程尝试用 CAS 建立轻量级锁失败，说明对象头已经发生变化。可能原因包括：",[21,1846,1847,1850,1853,1856],{},[24,1848,1849],{},"另一个线程持有轻量级锁；",[24,1851,1852],{},"对象已经膨胀为 ObjectMonitor；",[24,1854,1855],{},"竞争期间其他线程正在处理锁状态；",[24,1857,1858],{},"必须执行依赖 ObjectMonitor 的操作。",[14,1860,1861],{},"JVM 可以先进行短暂自旋，希望持锁线程很快退出。自旋避免了线程立即挂起和恢复，但会消耗 CPU，因此只适合临界区较短的情况。",[14,1863,1864],{},"当快速路径不能解决问题时，JVM 会执行 Monitor inflation：",[135,1866,1869],{"className":1867,"code":1868,"language":140,"meta":141},[138],"创建或取得 ObjectMonitor\n复制并保存原 Mark Word\n将对象头改为指向 ObjectMonitor\n由 ObjectMonitor 负责后续竞争\n",[77,1870,1868],{"__ignoreMap":141},[14,1872,1873],{},"对象头低两位变成：",[135,1875,1878],{"className":1876,"code":1877,"language":140,"meta":141},[138],"10\n",[77,1879,1877],{"__ignoreMap":141},[10,1881,1883],{"id":1882},"十三重量级锁与-objectmonitor","十三、重量级锁与 ObjectMonitor",[14,1885,1886,1887,1890],{},"重量级状态下，对象 Mark Word 指向一个 ",[77,1888,1889],{},"ObjectMonitor","。理解它时重点关注以下字段或逻辑角色：",[135,1892,1895],{"className":1893,"code":1894,"language":140,"meta":141},[138],"_owner       当前持有 Monitor 的线程或相关锁记录\n_recursions  重入次数\n_cxq         新到达竞争线程形成的竞争队列\n_EntryList   等待重新竞争 Owner 的线程\n_WaitSet     调用 wait 后等待通知的线程\n_header      膨胀前保存的对象 Mark Word\n",[77,1896,1894],{"__ignoreMap":141},[14,1898,1899],{},"概念结构：",[135,1901,1904],{"className":1902,"code":1903,"language":140,"meta":141},[138],"对象 Mark Word\n      │\n      ▼\nObjectMonitor\n  ├─ Owner\n  ├─ Recursions\n  ├─ cxq \u002F EntryList\n  ├─ WaitSet\n  └─ displaced header\n",[77,1905,1903],{"__ignoreMap":141},[14,1907,1908],{},"竞争线程可能先自旋尝试获得 Owner。仍然失败时，线程会进入等待结构并被挂起；持有线程退出后，Monitor 选择或唤醒后继线程继续竞争。",[14,1910,1911],{},"“重量级”的成本主要来自：",[21,1913,1914,1917,1920,1923],{},[24,1915,1916],{},"维护竞争和等待队列；",[24,1918,1919],{},"线程阻塞、唤醒与调度；",[24,1921,1922],{},"高竞争下的上下文切换；",[24,1924,1925],{},"对 Monitor 状态的原子协调。",[14,1927,1928],{},"它并不意味着每次操作都必然立刻执行一次昂贵的系统调用，HotSpot 仍包含快速路径和自适应自旋等优化。",[10,1930,1932],{"id":1931},"十四waitnotify-为什么会涉及重量级-monitor","十四、wait、notify 为什么会涉及重量级 Monitor",[14,1934,1935,1937],{},[77,1936,1535],{}," 的语义不是普通锁竞争：",[135,1939,1942],{"className":1940,"code":1941,"language":140,"meta":141},[138],"确认当前线程持有 Monitor\n完整释放锁及其重入层数\n进入 WaitSet\n等待 notify、notifyAll、中断或超时\n重新参与锁竞争\n恢复原重入状态后返回\n",[77,1943,1941],{"__ignoreMap":141},[14,1945,1946,1947,1949],{},"这需要 Owner、WaitSet、EntryList、重入次数等完整 Monitor 能力，因此 JDK 8 HotSpot 执行 ",[77,1948,1535],{}," 时会确保对象已经膨胀为 ObjectMonitor。",[14,1951,1952,1955,1956,1959],{},[77,1953,1954],{},"notify()"," 和 ",[77,1957,1958],{},"notifyAll()"," 存在一个细节：如果对象当前只是由调用线程栈锁定，那么它不可能已经拥有 WaitSet，HotSpot 可以直接返回而不做无意义的膨胀；否则会取得或膨胀 ObjectMonitor，再处理等待线程。",[14,1961,1962,1964],{},[77,1963,1954],{}," 只是把等待线程从“等待条件”推进到“可以重新竞争锁”的阶段，并不会让它绕过当前 Owner 立即执行。",[10,1966,1968],{"id":1967},"十五identityhashcode-对锁状态的影响","十五、identityHashCode 对锁状态的影响",[14,1970,1971],{},"普通未锁定对象可以把 identity hash 保存在 Mark Word 中，但偏向锁的 Mark Word 需要保存 JavaThread 指针。",[14,1973,1974],{},"因此，对偏向对象计算：",[135,1976,1980],{"className":1977,"code":1978,"language":1979,"meta":141,"style":141},"language-java shiki shiki-themes github-light github-dark","System.identityHashCode(obj);\n","java",[77,1981,1982],{"__ignoreMap":141},[495,1983,1984],{"class":497,"line":498},[495,1985,1978],{},[14,1987,1988],{},"通常会导致偏向撤销，使对象头能够保存 hash。",[14,1990,1991],{},"如果对象正在使用轻量级锁，原 Mark Word 已经保存在当前线程栈的 Lock Record 中。为了稳定保存 identity hash，并让其他线程可靠读取，HotSpot 的相关路径可能把对象膨胀为 ObjectMonitor，再把 hash 保存在 Monitor 保存的 header 中。",[14,1993,1994],{},"所以观察锁升级实验时，不要在不知情的情况下调用可能计算 identity hash 的方法，否则实验本身会改变对象头。",[10,1996,1998],{"id":1997},"十六完整状态转换表","十六、完整状态转换表",[230,2000,2001,2014],{},[233,2002,2003],{},[236,2004,2005,2008,2011],{},[239,2006,2007],{},"当前状态",[239,2009,2010],{},"触发条件",[239,2012,2013],{},"可能结果",[249,2015,2016,2029,2042,2055,2067,2079,2091,2104,2116,2128,2140,2151,2164],{},[236,2017,2018,2023,2026],{},[254,2019,2020,2021],{},"普通未锁定 ",[77,2022,1331],{},[254,2024,2025],{},"首次进入同步块",[254,2027,2028],{},"CAS 成功后成为轻量级锁",[236,2030,2031,2036,2039],{},[254,2032,2033,2034],{},"匿名偏向 ",[77,2035,1352],{},[254,2037,2038],{},"第一个线程进入",[254,2040,2041],{},"CAS 写入线程指针，成为已偏向状态",[236,2043,2044,2049,2052],{},[254,2045,2046,2047],{},"已偏向 ",[77,2048,1352],{},[254,2050,2051],{},"原线程再次进入",[254,2053,2054],{},"保持偏向，低成本重入",[236,2056,2057,2061,2064],{},[254,2058,2046,2059],{},[77,2060,1352],{},[254,2062,2063],{},"新线程访问且 epoch 过期",[254,2065,2066],{},"尝试重偏向或撤销",[236,2068,2069,2073,2076],{},[254,2070,2046,2071],{},[77,2072,1352],{},[254,2074,2075],{},"新线程访问，原线程未持锁",[254,2077,2078],{},"撤销为普通状态，或按策略重偏向",[236,2080,2081,2085,2088],{},[254,2082,2046,2083],{},[77,2084,1352],{},[254,2086,2087],{},"新线程访问，原线程仍持锁",[254,2089,2090],{},"撤销偏向，转轻量级或膨胀",[236,2092,2093,2098,2101],{},[254,2094,2095,2096],{},"轻量级 ",[77,2097,1376],{},[254,2099,2100],{},"当前线程重入",[254,2102,2103],{},"新增递归 Lock Record，保持轻量级",[236,2105,2106,2110,2113],{},[254,2107,2095,2108],{},[77,2109,1376],{},[254,2111,2112],{},"无竞争退出",[254,2114,2115],{},"CAS 恢复原 Mark Word",[236,2117,2118,2122,2125],{},[254,2119,2095,2120],{},[77,2121,1376],{},[254,2123,2124],{},"竞争持续或恢复对象头失败",[254,2126,2127],{},"膨胀为 ObjectMonitor",[236,2129,2130,2133,2138],{},[254,2131,2132],{},"任意适用状态",[254,2134,2135,2137],{},[77,2136,1535],{}," 等需要完整 Monitor 语义",[254,2139,2127],{},[236,2141,2142,2145,2148],{},[254,2143,2144],{},"偏向\u002F轻量级",[254,2146,2147],{},"计算 identity hash 等特殊场景",[254,2149,2150],{},"撤销偏向或膨胀",[236,2152,2153,2158,2161],{},[254,2154,2155,2156],{},"重量级 ",[77,2157,1394],{},[254,2159,2160],{},"Owner 退出",[254,2162,2163],{},"唤醒\u002F选择后继；对象仍可保持膨胀",[236,2165,2166,2169,2172],{},[254,2167,2168],{},"空闲 ObjectMonitor",[254,2170,2171],{},"后续安全点清理",[254,2173,2174],{},"JVM 可能执行 Monitor deflation",[10,2176,2178],{"id":2177},"十七三条典型执行路径","十七、三条典型执行路径",[43,2180,2182],{"id":2181},"路径一同一个线程反复进入","路径一：同一个线程反复进入",[135,2184,2187],{"className":2185,"code":2186,"language":140,"meta":141},[138],"匿名偏向\n  → CAS 偏向线程 A\n  → A 执行同步块\n  → A 退出但保留偏向\n  → A 再次进入，快速命中\n",[77,2188,2186],{"__ignoreMap":141},[43,2190,2192],{"id":2191},"路径二线程交替使用没有重叠竞争","路径二：线程交替使用，没有重叠竞争",[14,2194,2195],{},"偏向关闭或偏向已撤销时：",[135,2197,2200],{"className":2198,"code":2199,"language":140,"meta":141},[138],"普通未锁定\n  → A 建立轻量级锁\n  → A CAS 恢复对象头\n  → B 建立轻量级锁\n  → B CAS 恢复对象头\n",[77,2201,2199],{"__ignoreMap":141},[14,2203,2204],{},"这种场景没有必要让线程进入阻塞等待。",[43,2206,2208],{"id":2207},"路径三多线程同时竞争","路径三：多线程同时竞争",[135,2210,2213],{"className":2211,"code":2212,"language":140,"meta":141},[138],"A 持有轻量级锁\n  → B CAS 失败并短暂自旋\n  → A 未及时释放\n  → 对象膨胀为 ObjectMonitor\n  → B 进入竞争\u002F等待结构\n  → A 退出并推进后继竞争\n",[77,2214,2212],{"__ignoreMap":141},[10,2216,2218],{"id":2217},"十八jol-验证时要注意什么","十八、JOL 验证时要注意什么",[14,2220,2221],{},"可以使用 JOL 查看对象布局：",[135,2223,2227],{"className":2224,"code":2225,"language":2226,"meta":141,"style":141},"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",[77,2228,2229,2234,2239,2244,2249],{"__ignoreMap":141},[495,2230,2231],{"class":497,"line":498},[495,2232,2233],{},"\u003Cdependency>\n",[495,2235,2236],{"class":497,"line":504},[495,2237,2238],{},"    \u003CgroupId>org.openjdk.jol\u003C\u002FgroupId>\n",[495,2240,2241],{"class":497,"line":510},[495,2242,2243],{},"    \u003CartifactId>jol-core\u003C\u002FartifactId>\n",[495,2245,2246],{"class":497,"line":661},[495,2247,2248],{},"    \u003Cversion>0.17\u003C\u002Fversion>\n",[495,2250,2252],{"class":497,"line":2251},5,[495,2253,2254],{},"\u003C\u002Fdependency>\n",[14,2256,2257],{},"示例：",[135,2259,2261],{"className":1977,"code":2260,"language":1979,"meta":141,"style":141},"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",[77,2262,2263,2268,2273,2278,2282,2287,2293,2299,2304],{"__ignoreMap":141},[495,2264,2265],{"class":497,"line":498},[495,2266,2267],{},"Object lock = new Object();\n",[495,2269,2270],{"class":497,"line":504},[495,2271,2272],{"emptyLinePlaceholder":1086},"\n",[495,2274,2275],{"class":497,"line":510},[495,2276,2277],{},"System.out.println(ClassLayout.parseInstance(lock).toPrintable());\n",[495,2279,2280],{"class":497,"line":661},[495,2281,2272],{"emptyLinePlaceholder":1086},[495,2283,2284],{"class":497,"line":2251},[495,2285,2286],{},"synchronized (lock) {\n",[495,2288,2290],{"class":497,"line":2289},6,[495,2291,2292],{},"    System.out.println(ClassLayout.parseInstance(lock).toPrintable());\n",[495,2294,2296],{"class":497,"line":2295},7,[495,2297,2298],{},"}\n",[495,2300,2302],{"class":497,"line":2301},8,[495,2303,2272],{"emptyLinePlaceholder":1086},[495,2305,2307],{"class":497,"line":2306},9,[495,2308,2277],{},[14,2310,2311],{},"实验时要控制以下变量：",[21,2313,2314,2317,2323,2329,2332,2335],{},[24,2315,2316],{},"明确使用 JDK 8 HotSpot；",[24,2318,2319,2320,558],{},"记录是否开启 ",[77,2321,2322],{},"UseBiasedLocking",[24,2324,2325,2326,558],{},"记录 ",[77,2327,2328],{},"BiasedLockingStartupDelay",[24,2330,2331],{},"避免日志、调试器或工具意外计算 identity hash；",[24,2333,2334],{},"JOL 输出会受 32\u002F64 位、压缩指针和 GC 配置影响；",[24,2336,2337],{},"线程已经终止不代表任何场景都一定重偏向，仍要看 epoch 和撤销策略。",[14,2339,2340],{},"为了避免默认 4 秒延迟影响实验，可以显式设置：",[135,2342,2345],{"className":2343,"code":2344,"language":140,"meta":141},[138],"-XX:+UseBiasedLocking\n-XX:BiasedLockingStartupDelay=0\n",[77,2346,2344],{"__ignoreMap":141},[14,2348,2349],{},"关闭偏向锁、直接观察轻量级路径：",[135,2351,2354],{"className":2352,"code":2353,"language":140,"meta":141},[138],"-XX:-UseBiasedLocking\n",[77,2355,2353],{"__ignoreMap":141},[10,2357,2359],{"id":2358},"十九常见误区","十九、常见误区",[43,2361,2363],{"id":2362},"误区一新对象一定先是无锁再升级为偏向锁","误区一：新对象一定先是无锁，再升级为偏向锁",[14,2365,2366],{},"不准确。偏向能力启用后，支持偏向的类可以让新对象直接使用匿名偏向的 prototype header。",[43,2368,2370],{"id":2369},"误区二第二个线程出现就一定升级为重量级锁","误区二：第二个线程出现就一定升级为重量级锁",[14,2372,2373],{},"不准确。它可能触发重偏向、偏向撤销、轻量级锁，也可能在存在实际竞争时膨胀。",[43,2375,2377],{"id":2376},"误区三轻量级锁就是一直自旋","误区三：轻量级锁就是一直自旋",[14,2379,2380],{},"不准确。轻量级锁的核心是栈上 Lock Record 与对象头 CAS；自旋是竞争时可能采用的等待优化，不是轻量级锁的完整定义。",[43,2382,2384],{"id":2383},"误区四重量级锁完全由操作系统-mutex-等同实现","误区四：重量级锁完全由操作系统 Mutex 等同实现",[14,2386,2387],{},"过度简化。ObjectMonitor 是 HotSpot 的 JVM 级同步结构，内部会使用 CAS、自旋、队列、park\u002Funpark 等机制；线程最终阻塞和唤醒会依赖操作系统能力，但两者不能直接画等号。",[43,2389,2391],{"id":2390},"误区五锁一旦膨胀这个对象终身都是重量级锁","误区五：锁一旦膨胀，这个对象终身都是重量级锁",[14,2393,2394],{},"不准确。当前竞争阶段通常不会立即降级，但空闲 Monitor 后续可能由 JVM 在安全点执行 deflation。",[43,2396,2398],{"id":2397},"误区六这套流程适用于所有-jdk","误区六：这套流程适用于所有 JDK",[14,2400,2401],{},"不准确。本文讨论的是 JDK 8 HotSpot。偏向锁后来被默认禁用，现代 HotSpot 的锁实现也持续演进，因此理解具体机制时必须先限定版本。",[10,2403,2405],{"id":2404},"二十参考资料","二十、参考资料",[21,2407,2408,2415,2422,2429,2436],{},[24,2409,2410],{},[58,2411,2414],{"href":2412,"rel":2413},"https:\u002F\u002Fgithub.com\u002Fopenjdk\u002Fjdk8u\u002Fblob\u002Fmaster\u002Fhotspot\u002Fsrc\u002Fshare\u002Fvm\u002Foops\u002FmarkOop.hpp",[1016],"JDK 8 HotSpot：markOop.hpp",[24,2416,2417],{},[58,2418,2421],{"href":2419,"rel":2420},"https:\u002F\u002Fgithub.com\u002Fopenjdk\u002Fjdk8u\u002Fblob\u002Fmaster\u002Fhotspot\u002Fsrc\u002Fshare\u002Fvm\u002Fruntime\u002Fsynchronizer.cpp",[1016],"JDK 8 HotSpot：synchronizer.cpp",[24,2423,2424],{},[58,2425,2428],{"href":2426,"rel":2427},"https:\u002F\u002Fgithub.com\u002Fopenjdk\u002Fjdk8u\u002Fblob\u002Fmaster\u002Fhotspot\u002Fsrc\u002Fshare\u002Fvm\u002Fruntime\u002FbiasedLocking.cpp",[1016],"JDK 8 HotSpot：biasedLocking.cpp",[24,2430,2431],{},[58,2432,2435],{"href":2433,"rel":2434},"https:\u002F\u002Fgithub.com\u002Fopenjdk\u002Fjdk8u\u002Fblob\u002Fmaster\u002Fhotspot\u002Fsrc\u002Fshare\u002Fvm\u002Fruntime\u002FobjectMonitor.hpp",[1016],"JDK 8 HotSpot：objectMonitor.hpp",[24,2437,2438],{},[58,2439,2442],{"href":2440,"rel":2441},"https:\u002F\u002Fwww.cnblogs.com\u002Fstar95\u002Fp\u002F17542850.html",[1016],"参考文章：浅析 synchronized 锁升级的原理与实现",[10,2444,2445],{"id":2445},"相关内容",[21,2447,2448],{},[24,2449,2450],{},[58,2451,2453],{"href":2452},"\u002Fwiki\u002Fjava\u002Fsynchronized-vs-reentrantlock\u002F","synchronized 和 ReentrantLock 的区别",[1047,2455,1049],{},{"title":141,"searchDepth":504,"depth":504,"links":2457},[2458,2459,2460,2464,2469,2473,2477,2482,2486,2489,2490,2491,2492,2493,2494,2495,2496,2501,2502,2510,2511],{"id":1102,"depth":504,"text":1103},{"id":1163,"depth":504,"text":1164},{"id":1246,"depth":504,"text":1247,"children":2461},[2462,2463],{"id":1282,"depth":510,"text":1283},{"id":1448,"depth":510,"text":1449},{"id":1509,"depth":504,"text":1510,"children":2465},[2466,2467,2468],{"id":1516,"depth":510,"text":1516},{"id":1522,"depth":510,"text":1523},{"id":1529,"depth":510,"text":1529},{"id":1547,"depth":504,"text":1548,"children":2470},[2471,2472],{"id":1563,"depth":510,"text":1563},{"id":1578,"depth":510,"text":1578},{"id":1605,"depth":504,"text":1606,"children":2474},[2475,2476],{"id":1642,"depth":510,"text":1642},{"id":1657,"depth":510,"text":1657},{"id":1663,"depth":504,"text":1664,"children":2478},[2479,2480,2481],{"id":1673,"depth":510,"text":1674},{"id":1680,"depth":510,"text":1681},{"id":1687,"depth":510,"text":1688},{"id":1702,"depth":504,"text":1703,"children":2483},[2484,2485],{"id":1470,"depth":510,"text":1470},{"id":1723,"depth":510,"text":1723},{"id":1732,"depth":504,"text":1733,"children":2487},[2488],{"id":1780,"depth":510,"text":1781},{"id":1787,"depth":504,"text":1788},{"id":1812,"depth":504,"text":1813},{"id":1840,"depth":504,"text":1841},{"id":1882,"depth":504,"text":1883},{"id":1931,"depth":504,"text":1932},{"id":1967,"depth":504,"text":1968},{"id":1997,"depth":504,"text":1998},{"id":2177,"depth":504,"text":2178,"children":2497},[2498,2499,2500],{"id":2181,"depth":510,"text":2182},{"id":2191,"depth":510,"text":2192},{"id":2207,"depth":510,"text":2208},{"id":2217,"depth":504,"text":2218},{"id":2358,"depth":504,"text":2359,"children":2503},[2504,2505,2506,2507,2508,2509],{"id":2362,"depth":510,"text":2363},{"id":2369,"depth":510,"text":2370},{"id":2376,"depth":510,"text":2377},{"id":2383,"depth":510,"text":2384},{"id":2390,"depth":510,"text":2391},{"id":2397,"depth":510,"text":2398},{"id":2404,"depth":504,"text":2405},{"id":2445,"depth":504,"text":2445},"wiki:java:jdk8-synchronized-lock-states","从 Mark Word、Lock Record 与 ObjectMonitor 出发，系统理解 JDK 8 HotSpot 中偏向锁、轻量级锁、重量级锁的获取、撤销、膨胀和释放过程。",{},35,"\u002Fjava\u002Fjdk8-synchronized-lock-states","并发编程",{"title":1097,"description":2513},"java\u002Fjdk8-synchronized-lock-states","2026-07-23","gORBab_xEnsWFF1EDOBMo_opFfosXf7qG31N3VQ4jOM",{"id":2523,"title":2453,"body":2524,"commentId":3418,"description":3419,"difficulty":3420,"draft":1083,"extension":1084,"meta":3421,"navigation":1086,"order":3422,"path":3423,"section":1089,"seo":3424,"stem":3425,"updated":3426,"__hash__":3427},"java\u002Fjava\u002Fsynchronized-vs-reentrantlock.md",{"type":7,"value":2525,"toc":3397},[2526,2529,2533,2538,2541,2548,2550,2558,2568,2572,2574,2580,2583,2589,2592,2595,2601,2608,2611,2615,2767,2769,2773,2776,2781,2784,2790,2793,2796,2802,2805,2807,2811,2817,2820,2823,2829,2832,2837,2839,2843,2846,2849,2855,2858,2861,2867,2870,2876,2878,2884,2887,2889,2893,2896,2902,2905,2911,2916,2919,2921,2925,2930,2933,2939,2945,2951,2954,2960,2963,2969,2972,2978,2981,2987,2990,2996,2999,3001,3005,3008,3011,3017,3020,3026,3029,3032,3035,3041,3044,3047,3053,3056,3062,3069,3071,3075,3080,3086,3089,3092,3098,3104,3110,3113,3119,3122,3124,3128,3131,3134,3150,3153,3158,3161,3163,3167,3170,3190,3192,3198,3201,3206,3208,3212,3214,3240,3243,3249,3252,3254,3258,3303,3306,3311,3318,3324,3327,3329,3333,3337,3340,3344,3347,3351,3354,3362,3366,3369,3373,3376,3379,3385,3387,3390],[14,2527,2528],{},"synchronized和reentrantLock的区别？",[2530,2531,2532],"h1",{"id":2532},"面试标准回答",[47,2534,2535],{},[14,2536,2537],{},"synchronized 是 JVM 层面的内置锁，语法简单，进入同步块后自动加锁，退出时自动释放，支持可重入和内存可见性。ReentrantLock 是基于 AQS 实现的显式锁，同样支持可重入，但提供了公平锁、可响应中断获取锁、tryLock、超时获取以及多个 Condition 等高级能力。两者在现代 JDK 中性能差距通常不是主要选型依据；功能简单时优先 synchronized，需要精细控制时使用 ReentrantLock，并且必须在 finally 中释放锁。",[14,2539,2540],{},"一句话记忆：",[47,2542,2543],{},[14,2544,2545],{},[213,2546,2547],{},"synchronized 简单自动，ReentrantLock 灵活可控。",[2530,2549,54],{"id":54},[14,2551,2552,1955,2554,2557],{},[77,2553,1108],{},[77,2555,2556],{},"ReentrantLock"," 都能实现互斥和可见性，但定位不同：",[47,2559,2560],{},[14,2561,2562,2564,2565,2567],{},[77,2563,1108],{}," 是 JVM 原生关键字，语法简单；",[77,2566,2556],{}," 是 JUC 提供的显式锁，功能更丰富、控制能力更强。",[10,2569,2571],{"id":2570},"一基本用法","一、基本用法",[43,2573,1108],{"id":1108},[135,2575,2578],{"className":2576,"code":2577,"language":140},[138],"synchronized (lock) {\n    \u002F\u002F 临界区\n}\n",[77,2579,2577],{"__ignoreMap":141},[14,2581,2582],{},"或者：",[135,2584,2587],{"className":2585,"code":2586,"language":140},[138],"public synchronized void method() {\n}\n",[77,2588,2586],{"__ignoreMap":141},[14,2590,2591],{},"锁会自动释放。",[43,2593,2556],{"id":2594},"reentrantlock",[135,2596,2599],{"className":2597,"code":2598,"language":140},[138],"ReentrantLock lock = new ReentrantLock();\n\nlock.lock();\ntry {\n    \u002F\u002F 临界区\n} finally {\n    lock.unlock();\n}\n",[77,2600,2598],{"__ignoreMap":141},[14,2602,2603,2604,2607],{},"必须手动释放，所以通常必须写在 ",[77,2605,2606],{},"finally"," 中。",[2609,2610],"hr",{},[10,2612,2614],{"id":2613},"二核心区别","二、核心区别",[230,2616,2617,2627],{},[233,2618,2619],{},[236,2620,2621,2623,2625],{},[239,2622,241],{},[239,2624,1108],{},[239,2626,2556],{},[249,2628,2629,2640,2651,2661,2671,2684,2696,2708,2719,2734,2745,2756],{},[236,2630,2631,2634,2637],{},[254,2632,2633],{},"实现层次",[254,2635,2636],{},"JVM 关键字、Monitor",[254,2638,2639],{},"Java 类，基于 AQS",[236,2641,2642,2645,2648],{},[254,2643,2644],{},"加锁释放",[254,2646,2647],{},"自动",[254,2649,2650],{},"手动",[236,2652,2653,2656,2659],{},[254,2654,2655],{},"可重入",[254,2657,2658],{},"支持",[254,2660,2658],{},[236,2662,2663,2666,2669],{},[254,2664,2665],{},"公平锁",[254,2667,2668],{},"不支持显式配置",[254,2670,2658],{},[236,2672,2673,2676,2679],{},[254,2674,2675],{},"可中断获取锁",[254,2677,2678],{},"不支持",[254,2680,2681],{},[77,2682,2683],{},"lockInterruptibly()",[236,2685,2686,2689,2691],{},[254,2687,2688],{},"尝试获取锁",[254,2690,2678],{},[254,2692,2693],{},[77,2694,2695],{},"tryLock()",[236,2697,2698,2701,2703],{},[254,2699,2700],{},"超时获取锁",[254,2702,2678],{},[254,2704,2705],{},[77,2706,2707],{},"tryLock(timeout, unit)",[236,2709,2710,2713,2716],{},[254,2711,2712],{},"条件队列",[254,2714,2715],{},"一个 Monitor WaitSet",[254,2717,2718],{},"可创建多个 Condition",[236,2720,2721,2724,2729],{},[254,2722,2723],{},"等待通知",[254,2725,2726],{},[77,2727,2728],{},"wait\u002Fnotify\u002FnotifyAll",[254,2730,2731],{},[77,2732,2733],{},"await\u002Fsignal\u002FsignalAll",[236,2735,2736,2739,2742],{},[254,2737,2738],{},"锁状态查询",[254,2740,2741],{},"能力有限",[254,2743,2744],{},"提供较多查询方法",[236,2746,2747,2750,2753],{},[254,2748,2749],{},"编码复杂度",[254,2751,2752],{},"低",[254,2754,2755],{},"较高",[236,2757,2758,2761,2764],{},[254,2759,2760],{},"异常释放",[254,2762,2763],{},"自动释放",[254,2765,2766],{},"必须 finally unlock",[2609,2768],{},[2530,2770,2772],{"id":2771},"三两者都支持可重入","三、两者都支持可重入",[14,2774,2775],{},"可重入指：",[47,2777,2778],{},[14,2779,2780],{},"同一个线程已经持有锁时，可以再次获取同一把锁。",[43,2782,1108],{"id":2783},"synchronized-1",[135,2785,2788],{"className":2786,"code":2787,"language":140},[138],"public synchronized void methodA() {\n    methodB();\n}\n\npublic synchronized void methodB() {\n}\n",[77,2789,2787],{"__ignoreMap":141},[14,2791,2792],{},"同一个对象上的两个同步方法，线程可以重入。",[43,2794,2556],{"id":2795},"reentrantlock-1",[135,2797,2800],{"className":2798,"code":2799,"language":140},[138],"lock.lock();\ntry {\n    lock.lock();\n    try {\n        \u002F\u002F 重入\n    } finally {\n        lock.unlock();\n    }\n} finally {\n    lock.unlock();\n}\n",[77,2801,2799],{"__ignoreMap":141},[14,2803,2804],{},"获取几次，就必须释放几次。",[2609,2806],{},[2530,2808,2810],{"id":2809},"四reentrantlock-支持公平锁","四、ReentrantLock 支持公平锁",[135,2812,2815],{"className":2813,"code":2814,"language":140},[138],"ReentrantLock fairLock = new ReentrantLock(true);\n",[77,2816,2814],{"__ignoreMap":141},[14,2818,2819],{},"公平锁会尽量按 AQS 队列顺序获取锁。",[14,2821,2822],{},"默认是非公平锁：",[135,2824,2827],{"className":2825,"code":2826,"language":140},[138],"ReentrantLock lock = new ReentrantLock();\n",[77,2828,2826],{"__ignoreMap":141},[14,2830,2831],{},"非公平锁允许新线程插队，吞吐量通常更高。",[14,2833,2834,2836],{},[77,2835,1108],{}," 没有 API 让你指定公平性，通常按非公平竞争理解。",[2609,2838],{},[2530,2840,2842],{"id":2841},"五reentrantlock-支持可响应中断等待","五、ReentrantLock 支持可响应中断等待",[14,2844,2845],{},"假设线程正在等待锁。",[14,2847,2848],{},"使用：",[135,2850,2853],{"className":2851,"code":2852,"language":140},[138],"lock.lock();\n",[77,2854,2852],{"__ignoreMap":141},[14,2856,2857],{},"即使线程被中断，也不会因为中断立即退出获取锁过程。",[14,2859,2860],{},"而：",[135,2862,2865],{"className":2863,"code":2864,"language":140},[138],"lock.lockInterruptibly();\n",[77,2866,2864],{"__ignoreMap":141},[14,2868,2869],{},"等待期间如果收到中断，会抛出：",[135,2871,2874],{"className":2872,"code":2873,"language":140},[138],"InterruptedException\n",[77,2875,2873],{"__ignoreMap":141},[14,2877,106],{},[135,2879,2882],{"className":2880,"code":2881,"language":140},[138],"try {\n    lock.lockInterruptibly();\n    try {\n        doWork();\n    } finally {\n        lock.unlock();\n    }\n} catch (InterruptedException e) {\n    Thread.currentThread().interrupt();\n}\n",[77,2883,2881],{"__ignoreMap":141},[14,2885,2886],{},"这对于避免线程长时间死等、响应取消请求很有价值。",[2609,2888],{},[2530,2890,2892],{"id":2891},"六reentrantlock-支持尝试获取和超时","六、ReentrantLock 支持尝试获取和超时",[43,2894,2895],{"id":2895},"立即尝试",[135,2897,2900],{"className":2898,"code":2899,"language":140},[138],"if (lock.tryLock()) {\n    try {\n        doWork();\n    } finally {\n        lock.unlock();\n    }\n} else {\n    \u002F\u002F 获取失败，走降级逻辑\n}\n",[77,2901,2899],{"__ignoreMap":141},[43,2903,2904],{"id":2904},"超时尝试",[135,2906,2909],{"className":2907,"code":2908,"language":140},[138],"if (lock.tryLock(2, TimeUnit.SECONDS)) {\n    try {\n        doWork();\n    } finally {\n        lock.unlock();\n    }\n} else {\n    \u002F\u002F 两秒内未拿到锁\n}\n",[77,2910,2908],{"__ignoreMap":141},[14,2912,2913,2915],{},[77,2914,1108],{}," 一旦进入竞争，只能等待，无法直接设置超时。",[14,2917,2918],{},"这也是 ReentrantLock 在需要降级、超时控制时的重要优势。",[2609,2920],{},[2530,2922,2924],{"id":2923},"七condition-比-waitnotify-更灵活","七、Condition 比 wait\u002Fnotify 更灵活",[14,2926,2927,2929],{},[77,2928,1108],{}," 的一个 Monitor 只有一个 WaitSet。",[14,2931,2932],{},"例如阻塞队列里：",[135,2934,2937],{"className":2935,"code":2936,"language":140},[138],"生产者等待 notFull\n消费者等待 notEmpty\n",[77,2938,2936],{"__ignoreMap":141},[14,2940,2941,2942,2944],{},"使用 ",[77,2943,1108],{}," 时，生产者和消费者都在同一个 WaitSet 中，通常要：",[135,2946,2949],{"className":2947,"code":2948,"language":140},[138],"notifyAll();\n",[77,2950,2948],{"__ignoreMap":141},[14,2952,2953],{},"而 ReentrantLock 可以创建多个 Condition：",[135,2955,2958],{"className":2956,"code":2957,"language":140},[138],"Condition notFull = lock.newCondition();\nCondition notEmpty = lock.newCondition();\n",[77,2959,2957],{"__ignoreMap":141},[14,2961,2962],{},"生产者等待：",[135,2964,2967],{"className":2965,"code":2966,"language":140},[138],"while (queue.size() == capacity) {\n    notFull.await();\n}\n",[77,2968,2966],{"__ignoreMap":141},[14,2970,2971],{},"消费者等待：",[135,2973,2976],{"className":2974,"code":2975,"language":140},[138],"while (queue.isEmpty()) {\n    notEmpty.await();\n}\n",[77,2977,2975],{"__ignoreMap":141},[14,2979,2980],{},"生产完成：",[135,2982,2985],{"className":2983,"code":2984,"language":140},[138],"notEmpty.signal();\n",[77,2986,2984],{"__ignoreMap":141},[14,2988,2989],{},"消费完成：",[135,2991,2994],{"className":2992,"code":2993,"language":140},[138],"notFull.signal();\n",[77,2995,2993],{"__ignoreMap":141},[14,2997,2998],{},"这样可以精确唤醒，减少无效唤醒。",[2609,3000],{},[2530,3002,3004],{"id":3003},"八底层实现不同","八、底层实现不同",[10,3006,1108],{"id":3007},"synchronized-2",[14,3009,3010],{},"经典理解：",[135,3012,3015],{"className":3013,"code":3014,"language":140},[138],"对象头 Mark Word\n+\nMonitor\n+\nJVM 锁优化\n",[77,3016,3014],{"__ignoreMap":141},[14,3018,3019],{},"JDK 8 中可能经历：",[135,3021,3024],{"className":3022,"code":3023,"language":140},[138],"偏向锁\n→ 轻量级锁\n→ 重量级锁\n",[77,3025,3023],{"__ignoreMap":141},[14,3027,3028],{},"这是 HotSpot 的 JVM 实现优化。",[10,3030,2556],{"id":3031},"reentrantlock-2",[14,3033,3034],{},"主要基于：",[135,3036,3039],{"className":3037,"code":3038,"language":140},[138],"AbstractQueuedSynchronizer\n",[77,3040,3038],{"__ignoreMap":141},[14,3042,3043],{},"也就是 AQS。",[14,3045,3046],{},"核心状态：",[135,3048,3051],{"className":3049,"code":3050,"language":140},[138],"volatile int state;\n",[77,3052,3050],{"__ignoreMap":141},[14,3054,3055],{},"独占锁时：",[135,3057,3060],{"className":3058,"code":3059,"language":140},[138],"state = 0  未加锁\nstate = 1  第一次获取\nstate > 1  重入次数\n",[77,3061,3059],{"__ignoreMap":141},[14,3063,3064,3065,3068],{},"竞争失败的线程进入 CLH 变体同步队列，并通过 ",[77,3066,3067],{},"park\u002Funpark"," 等待和唤醒。",[2609,3070],{},[2530,3072,3074],{"id":3073},"九异常情况下的差异","九、异常情况下的差异",[14,3076,3077,3079],{},[77,3078,1108],{},"：",[135,3081,3084],{"className":3082,"code":3083,"language":140},[138],"synchronized (lock) {\n    throw new RuntimeException();\n}\n",[77,3085,3083],{"__ignoreMap":141},[14,3087,3088],{},"离开同步块时，JVM 自动释放锁。",[14,3090,3091],{},"ReentrantLock：",[135,3093,3096],{"className":3094,"code":3095,"language":140},[138],"lock.lock();\ndoWork();\nlock.unlock();\n",[77,3097,3095],{"__ignoreMap":141},[14,3099,797,3100,3103],{},[77,3101,3102],{},"doWork()"," 抛异常：",[135,3105,3108],{"className":3106,"code":3107,"language":140},[138],"unlock 没执行\n→ 锁永久未释放\n",[77,3109,3107],{"__ignoreMap":141},[14,3111,3112],{},"所以必须写：",[135,3114,3117],{"className":3115,"code":3116,"language":140},[138],"lock.lock();\ntry {\n    doWork();\n} finally {\n    lock.unlock();\n}\n",[77,3118,3116],{"__ignoreMap":141},[14,3120,3121],{},"这是 ReentrantLock 最常见的编码风险。",[2609,3123],{},[2530,3125,3127],{"id":3126},"十性能区别","十、性能区别",[14,3129,3130],{},"早期 JDK 中，ReentrantLock 性能常常明显优于 synchronized。",[14,3132,3133],{},"但 JDK 6 以后，JVM 对 synchronized 做了大量优化：",[21,3135,3136,3139,3141,3144,3147],{},[24,3137,3138],{},"偏向锁",[24,3140,1379],{},[24,3142,3143],{},"自旋",[24,3145,3146],{},"锁消除",[24,3148,3149],{},"锁粗化",[14,3151,3152],{},"现代 JDK 中：",[47,3154,3155],{},[14,3156,3157],{},"两者性能差异通常不是选型的首要依据。",[14,3159,3160],{},"应该根据功能需求选择，而不是简单认为 ReentrantLock 一定更快。",[2609,3162],{},[2530,3164,3166],{"id":3165},"十一什么时候使用-synchronized","十一、什么时候使用 synchronized",[14,3168,3169],{},"适合：",[21,3171,3172,3175,3178,3181,3184,3187],{},[24,3173,3174],{},"临界区简单",[24,3176,3177],{},"不需要公平锁",[24,3179,3180],{},"不需要超时获取",[24,3182,3183],{},"不需要中断锁等待",[24,3185,3186],{},"只有一个等待条件",[24,3188,3189],{},"希望代码简单、自动释放锁",[14,3191,106],{},[135,3193,3196],{"className":3194,"code":3195,"language":140},[138],"public synchronized void increment() {\n    count++;\n}\n",[77,3197,3195],{"__ignoreMap":141},[14,3199,3200],{},"一般优先原则：",[47,3202,3203],{},[14,3204,3205],{},"能用 synchronized 清晰表达时，优先用 synchronized。",[2609,3207],{},[2530,3209,3211],{"id":3210},"十二什么时候使用-reentrantlock","十二、什么时候使用 ReentrantLock",[14,3213,3169],{},[21,3215,3216,3219,3225,3228,3231,3234,3237],{},[24,3217,3218],{},"需要公平锁",[24,3220,3221,3222],{},"需要 ",[77,3223,3224],{},"tryLock",[24,3226,3227],{},"需要超时获取锁",[24,3229,3230],{},"需要可中断等待",[24,3232,3233],{},"需要多个 Condition",[24,3235,3236],{},"需要查询等待队列、持锁状态",[24,3238,3239],{},"需要更精细的锁控制",[14,3241,3242],{},"例如转账时避免死锁：",[135,3244,3247],{"className":3245,"code":3246,"language":140},[138],"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",[77,3248,3246],{"__ignoreMap":141},[14,3250,3251],{},"使用 synchronized 很难实现这种超时退避。",[2609,3253],{},[2530,3255,3257],{"id":3256},"十三waitnotify-和-awaitsignal-对应关系","十三、wait\u002Fnotify 和 await\u002Fsignal 对应关系",[230,3259,3260,3268],{},[233,3261,3262],{},[236,3263,3264,3266],{},[239,3265,1108],{},[239,3267,2556],{},[249,3269,3270,3281,3292],{},[236,3271,3272,3276],{},[254,3273,3274],{},[77,3275,1535],{},[254,3277,3278],{},[77,3279,3280],{},"Condition.await()",[236,3282,3283,3287],{},[254,3284,3285],{},[77,3286,1954],{},[254,3288,3289],{},[77,3290,3291],{},"Condition.signal()",[236,3293,3294,3298],{},[254,3295,3296],{},[77,3297,1958],{},[254,3299,3300],{},[77,3301,3302],{},"Condition.signalAll()",[14,3304,3305],{},"两者都要求：",[47,3307,3308],{},[14,3309,3310],{},"调用等待或通知方法前，必须先持有对应的锁。",[14,3312,3313,3314,3317],{},"并且等待都应该放在 ",[77,3315,3316],{},"while"," 中：",[135,3319,3322],{"className":3320,"code":3321,"language":140},[138],"while (!conditionSatisfied()) {\n    condition.await();\n}\n",[77,3323,3321],{"__ignoreMap":141},[14,3325,3326],{},"防止虚假唤醒和竞争后条件失效。",[2609,3328],{},[2530,3330,3332],{"id":3331},"十四常见误区","十四、常见误区",[43,3334,3336],{"id":3335},"误区一reentrantlock-才支持可重入","误区一：ReentrantLock 才支持可重入",[14,3338,3339],{},"不对。两者都可重入。",[43,3341,3343],{"id":3342},"误区二reentrantlock-一定比-synchronized-快","误区二：ReentrantLock 一定比 synchronized 快",[14,3345,3346],{},"不对。现代 JVM 下要看场景，功能差异比纯性能更重要。",[43,3348,3350],{"id":3349},"误区三synchronized-会自动释放reentrantlock-不会","误区三：synchronized 会自动释放，ReentrantLock 不会",[14,3352,3353],{},"更准确地说：",[21,3355,3356,3359],{},[24,3357,3358],{},"synchronized 离开同步块时自动释放",[24,3360,3361],{},"ReentrantLock 必须显式 unlock",[43,3363,3365],{"id":3364},"误区四公平锁一定更好","误区四：公平锁一定更好",[14,3367,3368],{},"公平锁吞吐量通常更低，只在有明确公平性需求时使用。",[43,3370,3372],{"id":3371},"误区五trylock-失败后可以直接-unlock","误区五：tryLock 失败后可以直接 unlock",[14,3374,3375],{},"不可以。只有成功获取锁后才能释放。",[14,3377,3378],{},"正确写法：",[135,3380,3383],{"className":3381,"code":3382,"language":140},[138],"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",[77,3384,3382],{"__ignoreMap":141},[2609,3386],{},[10,3388,3389],{"id":3389},"相关主题",[21,3391,3392],{},[24,3393,3394],{},[58,3395,1097],{"href":3396},"\u002Fwiki\u002Fjava\u002Fjdk8-synchronized-lock-states\u002F",{"title":141,"searchDepth":504,"depth":504,"links":3398},[3399,3403,3409,3410,3417],{"id":2570,"depth":504,"text":2571,"children":3400},[3401,3402],{"id":1108,"depth":510,"text":1108},{"id":2594,"depth":510,"text":2556},{"id":2613,"depth":504,"text":2614,"children":3404},[3405,3406,3407,3408],{"id":2783,"depth":510,"text":1108},{"id":2795,"depth":510,"text":2556},{"id":2895,"depth":510,"text":2895},{"id":2904,"depth":510,"text":2904},{"id":3007,"depth":504,"text":1108},{"id":3031,"depth":504,"text":2556,"children":3411},[3412,3413,3414,3415,3416],{"id":3335,"depth":510,"text":3336},{"id":3342,"depth":510,"text":3343},{"id":3349,"depth":510,"text":3350},{"id":3364,"depth":510,"text":3365},{"id":3371,"depth":510,"text":3372},{"id":3389,"depth":504,"text":3389},"wiki:java:synchronized-vs-reentrantlock","对比 synchronized 与 ReentrantLock 的实现、可重入、公平锁、中断、超时、Condition 和适用场景。","intermediate",{},71,"\u002Fjava\u002Fsynchronized-vs-reentrantlock",{"title":2453,"description":3419},"java\u002Fsynchronized-vs-reentrantlock","2026-07-21","vNahPOMAudgb7Mu0G7t_sn5sjTaBeUGcX25bRe10EXg",{"id":3429,"title":3430,"body":3431,"commentId":4605,"description":4606,"difficulty":1082,"draft":1083,"extension":1084,"meta":4607,"navigation":1086,"order":4608,"path":4609,"section":1089,"seo":4610,"stem":4611,"updated":4612,"__hash__":4613},"java\u002Fjava\u002Fkafka-message-loss-prevention.md","Kafka 如何保证消息不丢失",{"type":7,"value":3432,"toc":4561},[3433,3436,3438,3455,3458,3465,3467,3470,3477,3480,3489,3493,3496,3510,3513,3517,3523,3530,3585,3588,3599,3608,3612,3615,3644,3647,3650,3664,3668,3671,3680,3687,3722,3729,3733,3736,3742,3745,3748,3754,3757,3761,3765,3768,3774,3777,3780,3784,3787,3790,3796,3799,3805,3810,3816,3819,3825,3828,3833,3836,3840,3849,3855,3858,3867,3870,3876,3886,3891,3894,3898,3902,3911,3914,3920,3923,3927,3930,3936,3939,3985,3988,3992,3995,4001,4004,4021,4028,4032,4035,4041,4044,4053,4056,4059,4063,4066,4072,4076,4079,4082,4088,4091,4094,4114,4121,4124,4130,4136,4140,4143,4145,4151,4157,4160,4174,4178,4249,4252,4259,4263,4313,4316,4323,4326,4330,4440,4444,4451,4459,4463,4469,4473,4476,4480,4483,4487,4490,4494,4497,4517,4520,4522,4559],[2530,3434,3430],{"id":3435},"kafka-如何保证消息不丢失",[10,3437,12],{"id":12},[47,3439,3440],{},[14,3441,3442,3443,3446,3447,3450,3451,3454],{},"Kafka 保证消息不丢失需要覆盖生产者、Broker 和消费者三段链路。生产者侧使用 ",[77,3444,3445],{},"acks=all","、开启重试和幂等生产，并检查发送回调，对最终失败进行落库、告警或补偿；Broker 侧通常设置三副本、",[77,3448,3449],{},"min.insync.replicas=2","，并关闭非同步副本选举，即配置 ",[77,3452,3453],{},"unclean.leader.election.enable=false","，保证消息在足够多的同步副本写入后才确认成功；消费者侧关闭自动提交，在业务处理成功后再提交 Offset，失败时不提交以便重新消费。这样通常实现至少一次投递，所以消费端还必须通过事件 ID、唯一索引或状态机保证幂等。涉及数据库与 Kafka 的一致性时，可以使用 Transactional Outbox；消费端需要应对重复消息时，可以使用 Inbox。Kafka 的消息不丢失是端到端保障，不是只配置一个参数。",[14,3456,3457],{},"一句话总结：",[47,3459,3460],{},[14,3461,3462],{},[213,3463,3464],{},"生产者确认成功，Broker 多副本可靠保存，消费者处理成功后提交，业务端用幂等和补偿兜底。",[10,3466,54],{"id":54},[14,3468,3469],{},"Kafka 消息不丢失不是靠某一个参数实现的，而是一个端到端问题：",[47,3471,3472],{},[14,3473,3474],{},[213,3475,3476],{},"生产者必须确认消息发送成功，Broker 必须把消息可靠地保存在足够多的副本中，消费者必须在业务处理成功后再提交 Offset。",[14,3478,3479],{},"其中任何一个环节处理不当，都可能出现“业务上看起来消息丢了”的情况。",[14,3481,3482],{},[58,3483,3486],{"href":3484,"target":61,"rel":3485},"\u002Fimages\u002Fwiki\u002Fjava\u002Fkafka-message-loss-prevention.svg",[63,64],[66,3487],{"src":3484,"alt":3488},"Kafka 消息不丢失的端到端保障链路",[10,3490,3492],{"id":3491},"一先明确什么叫消息丢失","一、先明确什么叫“消息丢失”",[14,3494,3495],{},"常见的丢失场景主要有四类：",[705,3497,3498,3501,3504,3507],{},[24,3499,3500],{},"生产者调用发送方法后没有检查结果，实际发送失败却被业务忽略。",[24,3502,3503],{},"Leader 收到消息后就返回成功，消息还未复制到 Follower，Leader 随后故障。",[24,3505,3506],{},"消费者先提交 Offset，再执行业务；业务处理失败后，消息不会再次投递。",[24,3508,3509],{},"Kafka 中的消息没有丢，但写数据库、调用下游等业务操作失败，又没有补偿机制。",[14,3511,3512],{},"因此，Kafka 中仍能查到消息，不代表业务一定处理成功；反过来，业务没有结果，也不一定是 Broker 丢了消息。",[10,3514,3516],{"id":3515},"二生产者如何避免丢消息","二、生产者如何避免丢消息",[43,3518,3520,3521],{"id":3519},"_1-使用-acksall","1. 使用 ",[77,3522,3445],{},[14,3524,3525,3526,3529],{},"生产者的 ",[77,3527,3528],{},"acks"," 决定 Broker 在什么条件下确认发送成功：",[230,3531,3532,3545],{},[233,3533,3534],{},[236,3535,3536,3539,3542],{},[239,3537,3538],{},"配置",[239,3540,3541],{},"确认时机",[239,3543,3544],{},"风险",[249,3546,3547,3560,3573],{},[236,3548,3549,3554,3557],{},[254,3550,3551],{},[77,3552,3553],{},"acks=0",[254,3555,3556],{},"不等待 Broker 响应",[254,3558,3559],{},"发送失败也无法感知，风险最高",[236,3561,3562,3567,3570],{},[254,3563,3564],{},[77,3565,3566],{},"acks=1",[254,3568,3569],{},"Leader 写入本地日志后返回",[254,3571,3572],{},"Leader 故障且副本尚未同步时可能丢失",[236,3574,3575,3579,3582],{},[254,3576,3577],{},[77,3578,3445],{},[254,3580,3581],{},"当前 ISR 中的副本都确认后返回",[254,3583,3584],{},"Kafka 能提供的最强生产者确认保证",[14,3586,3587],{},"关键配置：",[135,3589,3593],{"className":3590,"code":3591,"language":3592,"meta":141,"style":141},"language-properties shiki shiki-themes github-light github-dark","acks=all\n","properties",[77,3594,3595],{"__ignoreMap":141},[495,3596,3597],{"class":497,"line":498},[495,3598,3591],{},[14,3600,3601,3603,3604,3607],{},[77,3602,3445],{}," 并不等于绝对不丢。它还需要与副本数和 ",[77,3605,3606],{},"min.insync.replicas"," 配合，否则 ISR 中只剩 Leader 一个副本时，依然可能成功写入。",[43,3609,3611],{"id":3610},"_2-开启重试和幂等生产","2. 开启重试和幂等生产",[14,3613,3614],{},"网络抖动、Leader 切换等临时故障可能导致发送失败，生产者需要允许重试：",[135,3616,3618],{"className":3590,"code":3617,"language":3592,"meta":141,"style":141},"enable.idempotence=true\nacks=all\nretries=2147483647\nmax.in.flight.requests.per.connection=5\ndelivery.timeout.ms=120000\n",[77,3619,3620,3625,3629,3634,3639],{"__ignoreMap":141},[495,3621,3622],{"class":497,"line":498},[495,3623,3624],{},"enable.idempotence=true\n",[495,3626,3627],{"class":497,"line":504},[495,3628,3591],{},[495,3630,3631],{"class":497,"line":510},[495,3632,3633],{},"retries=2147483647\n",[495,3635,3636],{"class":497,"line":661},[495,3637,3638],{},"max.in.flight.requests.per.connection=5\n",[495,3640,3641],{"class":497,"line":2251},[495,3642,3643],{},"delivery.timeout.ms=120000\n",[14,3645,3646],{},"幂等生产者会给消息附加 Producer ID 和序列号，使 Broker 能识别同一生产会话内的重复写入，避免因重试产生重复消息。",[14,3648,3649],{},"需要注意：",[21,3651,3652,3655,3661],{},[24,3653,3654],{},"幂等生产解决的是重试导致的重复写入，不是发送失败后的业务补偿。",[24,3656,3657,3660],{},[77,3658,3659],{},"delivery.timeout.ms"," 到期后，生产者仍可能最终失败。",[24,3662,3663],{},"业务不能无限依赖客户端重试，最终失败必须记录、告警或进入补偿流程。",[43,3665,3667],{"id":3666},"_3-必须检查发送结果","3. 必须检查发送结果",[14,3669,3670],{},"下面这种“只发送、不处理结果”的写法存在风险：",[135,3672,3674],{"className":1977,"code":3673,"language":1979,"meta":141,"style":141},"producer.send(record);\n",[77,3675,3676],{"__ignoreMap":141},[495,3677,3678],{"class":497,"line":498},[495,3679,3673],{},[14,3681,3682,3683,3686],{},"应该检查回调或 ",[77,3684,3685],{},"Future"," 的执行结果：",[135,3688,3690],{"className":1977,"code":3689,"language":1979,"meta":141,"style":141},"producer.send(record, (metadata, exception) -> {\n    if (exception != null) {\n        \u002F\u002F 记录原始消息、告警并进入补偿流程\n        handleSendFailure(record, exception);\n    }\n});\n",[77,3691,3692,3697,3702,3707,3712,3717],{"__ignoreMap":141},[495,3693,3694],{"class":497,"line":498},[495,3695,3696],{},"producer.send(record, (metadata, exception) -> {\n",[495,3698,3699],{"class":497,"line":504},[495,3700,3701],{},"    if (exception != null) {\n",[495,3703,3704],{"class":497,"line":510},[495,3705,3706],{},"        \u002F\u002F 记录原始消息、告警并进入补偿流程\n",[495,3708,3709],{"class":497,"line":661},[495,3710,3711],{},"        handleSendFailure(record, exception);\n",[495,3713,3714],{"class":497,"line":2251},[495,3715,3716],{},"    }\n",[495,3718,3719],{"class":497,"line":2289},[495,3720,3721],{},"});\n",[14,3723,3724,3725,3728],{},"序列化失败、鉴权失败、消息过大和超时等错误，最终都需要业务明确处理。不能把“调用过 ",[77,3726,3727],{},"send","”当成“消息已经可靠进入 Kafka”。",[43,3730,3732],{"id":3731},"_4-数据库与-kafka-的一致性","4. 数据库与 Kafka 的一致性",[14,3734,3735],{},"如果业务流程是：",[135,3737,3740],{"className":3738,"code":3739,"language":140,"meta":141},[138],"更新数据库\n→\n发送 Kafka 消息\n",[77,3741,3739],{"__ignoreMap":141},[14,3743,3744],{},"数据库更新成功、消息发送失败时，仍然会造成业务事件丢失。",[14,3746,3747],{},"常见解决方式是本地消息表，也叫 Transactional Outbox：",[135,3749,3752],{"className":3750,"code":3751,"language":140,"meta":141},[138],"同一个数据库事务\n├── 更新业务数据\n└── 写入待发送事件表\n\n事务提交后\n→ 后台任务投递 Kafka\n→ 成功后标记事件已发送\n",[77,3753,3751],{"__ignoreMap":141},[14,3755,3756],{},"这样即使 Kafka 暂时不可用，消息也可以从本地消息表继续重试。Kafka 事务可以保证 Kafka 内部多条记录及消费 Offset 的原子性，但不会自动把外部数据库事务包含进来。",[10,3758,3760],{"id":3759},"三broker-如何保证消息可靠保存","三、Broker 如何保证消息可靠保存",[43,3762,3764],{"id":3763},"_1-合理设置副本数","1. 合理设置副本数",[14,3766,3767],{},"生产环境通常使用：",[135,3769,3772],{"className":3770,"code":3771,"language":140,"meta":141},[138],"replication.factor = 3\n",[77,3773,3771],{"__ignoreMap":141},[14,3775,3776],{},"一个分区包含一个 Leader 和多个 Follower。生产者与 Leader 交互，Follower 从 Leader 复制日志。某个 Broker 故障后，可以从仍然同步的副本中选举新 Leader。",[14,3778,3779],{},"副本数提高的是容错能力，但副本只有真正保持同步才有意义。",[43,3781,3783],{"id":3782},"_2-理解-isr","2. 理解 ISR",[14,3785,3786],{},"ISR 是 In-Sync Replicas，即当前与 Leader 保持同步的副本集合。",[14,3788,3789],{},"例如一个三副本分区：",[135,3791,3794],{"className":3792,"code":3793,"language":140,"meta":141},[138],"Leader A\nFollower B\nFollower C\n\nISR = [A, B, C]\n",[77,3795,3793],{"__ignoreMap":141},[14,3797,3798],{},"如果 C 长时间跟不上 Leader，它会被移出 ISR：",[135,3800,3803],{"className":3801,"code":3802,"language":140,"meta":141},[138],"ISR = [A, B]\n",[77,3804,3802],{"__ignoreMap":141},[14,3806,3807,3809],{},[77,3808,3445],{}," 等待的是当前 ISR 的确认，而不是永远等待所有配置副本。",[43,3811,3813,3814],{"id":3812},"_3-配置-mininsyncreplicas","3. 配置 ",[77,3815,3606],{},[14,3817,3818],{},"推荐组合：",[135,3820,3823],{"className":3821,"code":3822,"language":140,"meta":141},[138],"replication.factor = 3\nmin.insync.replicas = 2\nacks = all\n",[77,3824,3822],{"__ignoreMap":141},[14,3826,3827],{},"它表达的含义是：",[47,3829,3830],{},[14,3831,3832],{},"至少要有两个同步副本可用，写入才允许成功。",[14,3834,3835],{},"如果 ISR 只剩一个副本，Broker 会拒绝写入。此时系统牺牲部分可用性，避免在单副本状态下继续写入并承担更高的数据丢失风险。",[43,3837,3839],{"id":3838},"_4-关闭非同步副本选举","4. 关闭非同步副本选举",[135,3841,3843],{"className":3590,"code":3842,"language":3592,"meta":141,"style":141},"unclean.leader.election.enable=false\n",[77,3844,3845],{"__ignoreMap":141},[495,3846,3847],{"class":497,"line":498},[495,3848,3842],{},[14,3850,3851,3854],{},[77,3852,3853],{},"unclean leader election"," 指允许不在 ISR 中的副本被选举为 Leader。如果所有同步副本都不可用，非 ISR 副本可能缺少最新消息；将它选为 Leader 虽然能更快恢复分区服务，却可能造成日志回退和数据丢失。",[14,3856,3857],{},"因此，对数据可靠性要求较高的场景应明确配置：",[47,3859,3860],{},[14,3861,3862],{},[213,3863,3864,3865,401],{},"关闭非同步副本选举：",[77,3866,3453],{},[14,3868,3869],{},"这是一个典型的取舍：",[135,3871,3874],{"className":3872,"code":3873,"language":140,"meta":141},[138],"开启非同步副本选举：可用性更高，但可能丢失消息\n关闭非同步副本选举：数据可靠性更强，但分区可能暂时不可用\n",[77,3875,3873],{"__ignoreMap":141},[43,3877,3879,3880,3882,3883],{"id":3878},"_5-acksall-不等于每条消息都立即-fsync","5. ",[77,3881,3445],{}," 不等于每条消息都立即 ",[77,3884,3885],{},"fsync",[14,3887,3888,3889,401],{},"Kafka 的持久性主要依赖顺序写日志、操作系统页缓存和多副本机制。Broker 返回成功，并不表示每个副本都对该消息单独执行了一次物理磁盘 ",[77,3890,3885],{},[14,3892,3893],{},"实际生产中，通常通过跨 Broker 副本降低单机和单盘故障风险，而不是强制每条消息同步刷盘。若多个副本同时发生不可恢复故障，仍不存在数学意义上的绝对零丢失。",[10,3895,3897],{"id":3896},"四消费者如何避免消费丢失","四、消费者如何避免“消费丢失”",[43,3899,3901],{"id":3900},"_1-关闭自动提交-offset","1. 关闭自动提交 Offset",[135,3903,3905],{"className":3590,"code":3904,"language":3592,"meta":141,"style":141},"enable.auto.commit=false\n",[77,3906,3907],{"__ignoreMap":141},[495,3908,3909],{"class":497,"line":498},[495,3910,3904],{},[14,3912,3913],{},"自动提交可能出现以下顺序：",[135,3915,3918],{"className":3916,"code":3917,"language":140,"meta":141},[138],"拉取消息\n→ 自动提交 Offset\n→ 执行业务\n→ 进程崩溃\n",[77,3919,3917],{"__ignoreMap":141},[14,3921,3922],{},"重启后，消费者会从已提交的下一个 Offset 继续消费，刚才尚未处理成功的消息就被跳过了。",[43,3924,3926],{"id":3925},"_2-业务成功后再提交-offset","2. 业务成功后再提交 Offset",[14,3928,3929],{},"更可靠的顺序是：",[135,3931,3934],{"className":3932,"code":3933,"language":140,"meta":141},[138],"拉取消息\n→ 执行业务\n→ 业务成功\n→ 提交 Offset\n",[77,3935,3933],{"__ignoreMap":141},[14,3937,3938],{},"如果业务处理失败，就不提交 Offset，让消息后续重新消费：",[135,3940,3942],{"className":1977,"code":3941,"language":1979,"meta":141,"style":141},"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",[77,3943,3944,3949,3954,3958,3963,3968,3972,3976,3981],{"__ignoreMap":141},[495,3945,3946],{"class":497,"line":498},[495,3947,3948],{},"while (true) {\n",[495,3950,3951],{"class":497,"line":504},[495,3952,3953],{},"    ConsumerRecords\u003CString, String> records = consumer.poll(Duration.ofSeconds(1));\n",[495,3955,3956],{"class":497,"line":510},[495,3957,2272],{"emptyLinePlaceholder":1086},[495,3959,3960],{"class":497,"line":661},[495,3961,3962],{},"    for (ConsumerRecord\u003CString, String> record : records) {\n",[495,3964,3965],{"class":497,"line":2251},[495,3966,3967],{},"        process(record);\n",[495,3969,3970],{"class":497,"line":2289},[495,3971,3716],{},[495,3973,3974],{"class":497,"line":2295},[495,3975,2272],{"emptyLinePlaceholder":1086},[495,3977,3978],{"class":497,"line":2301},[495,3979,3980],{},"    consumer.commitSync();\n",[495,3982,3983],{"class":497,"line":2306},[495,3984,2298],{},[14,3986,3987],{},"示例表达的是基本原则。实际批量消费时，如果一批消息中只有部分成功，需要准确管理各分区可提交的 Offset，不能直接把失败消息之后的位置一起提交。",[43,3989,3991],{"id":3990},"_3-为什么还需要业务幂等","3. 为什么还需要业务幂等",[14,3993,3994],{},"“处理成功后提交”可以避免消息被跳过，但会引入重复：",[135,3996,3999],{"className":3997,"code":3998,"language":140,"meta":141},[138],"业务处理成功\n→ 提交 Offset 前进程崩溃\n→ 重启后再次消费同一条消息\n",[77,4000,3998],{"__ignoreMap":141},[14,4002,4003],{},"因此，可靠消费通常采用至少一次投递，并在业务层保证幂等。常见方式包括：",[21,4005,4006,4009,4012,4015,4018],{},[24,4007,4008],{},"使用事件 ID 建立数据库唯一索引。",[24,4010,4011],{},"建立消费去重表或 Inbox 表。",[24,4013,4014],{},"使用业务状态机，只允许合法状态转换。",[24,4016,4017],{},"更新时带版本号或业务条件。",[24,4019,4020],{},"对扣款、发券等操作使用唯一业务流水号。",[47,4022,4023],{},[14,4024,4025],{},[213,4026,4027],{},"不丢消息通常意味着允许重复，再通过幂等消除重复影响。",[43,4029,4031],{"id":4030},"_4-kafka-内部链路的-exactly-once","4. Kafka 内部链路的 Exactly Once",[14,4033,4034],{},"如果消费 Kafka 后仍然写回 Kafka，可以使用 Kafka 事务，把输出记录和消费 Offset 放进同一个事务：",[135,4036,4039],{"className":4037,"code":4038,"language":140,"meta":141},[138],"消费消息\n→ 处理\n→ 写入下游 Topic\n→ sendOffsetsToTransaction\n→ 提交事务\n",[77,4040,4038],{"__ignoreMap":141},[14,4042,4043],{},"下游消费者设置：",[135,4045,4047],{"className":3590,"code":4046,"language":3592,"meta":141,"style":141},"isolation.level=read_committed\n",[77,4048,4049],{"__ignoreMap":141},[495,4050,4051],{"class":497,"line":498},[495,4052,4046],{},[14,4054,4055],{},"这样只读取已提交事务的数据。",[14,4057,4058],{},"但如果最终写入的是 MySQL 等外部系统，Kafka 事务并不能自动保证 Kafka 与数据库的原子性。生产方可以用 Outbox 保证事件可靠发出，消费方可以用 Inbox 应对重复消费，具体区别见下一节。",[10,4060,4062],{"id":4061},"五outbox-与-inbox-分别是什么","五、Outbox 与 Inbox 分别是什么",[14,4064,4065],{},"Outbox 和 Inbox 都借助数据库本地事务解决消息链路中的一致性问题，但它们位于不同位置：",[135,4067,4070],{"className":4068,"code":4069,"language":140,"meta":141},[138],"业务生产方 → Outbox → Kafka → Inbox → 业务消费方\n",[77,4071,4069],{"__ignoreMap":141},[43,4073,4075],{"id":4074},"_1-outbox保证业务事件可靠发出","1. Outbox：保证业务事件可靠发出",[14,4077,4078],{},"Outbox 位于消息生产方，用来解决“数据库更新成功，但 Kafka 消息发送失败”的双写不一致问题。",[14,4080,4081],{},"核心流程：",[135,4083,4086],{"className":4084,"code":4085,"language":140,"meta":141},[138],"同一个数据库事务\n├── 更新业务数据\n└── 写入 Outbox 事件表\n\n事务提交\n→ 后台投递程序扫描 Outbox\n→ 发送 Kafka\n→ 收到成功确认后标记已发送\n",[77,4087,4085],{"__ignoreMap":141},[14,4089,4090],{},"例如创建订单时，不直接把“订单创建事件”的可靠性寄托在一次 Kafka 调用上，而是在创建订单的同一个数据库事务中写入一条 Outbox 记录。即使 Kafka 暂时不可用，后台任务仍可继续重试。",[14,4092,4093],{},"Outbox 的关键点：",[21,4095,4096,4099,4105,4108,4111],{},[24,4097,4098],{},"业务数据和事件记录必须写入同一个数据库事务。",[24,4100,4101,4102,401],{},"每条事件应有全局唯一的 ",[77,4103,4104],{},"eventId",[24,4106,4107],{},"投递程序可以轮询事件表，也可以通过 CDC 捕获新增事件。",[24,4109,4110],{},"“发送成功但标记失败”可能导致再次投递，因此消费者仍需幂等。",[24,4112,4113],{},"Outbox 解决的是事件可靠发出，不保证消费者一定处理成功。",[14,4115,4116,4117,4120],{},"这里的 CDC 是 ",[213,4118,4119],{},"Change Data Capture（变更数据捕获）","。它会读取数据库的变更日志，例如 MySQL Binlog，捕获数据的新增、修改和删除，再把变更发送到 Kafka。常用工具有 Debezium、Canal。",[14,4122,4123],{},"在 Outbox 场景中，它的工作链路是：",[135,4125,4128],{"className":4126,"code":4127,"language":140,"meta":141},[138],"业务事务写入业务表和 Outbox 表\n→ CDC 监听 Binlog\n→ 捕获新增的 Outbox 记录\n→ 发送到 Kafka\n",[77,4129,4127],{"__ignoreMap":141},[14,4131,4132,4133,4135],{},"相比定时扫描 Outbox 表，CDC 延迟更低，也能减少频繁查询数据库的压力。但消息链路仍可能产生重复，因此必须保留 ",[77,4134,4104],{},"，并由消费端保证幂等。",[43,4137,4139],{"id":4138},"_2-inbox保证重复消息只产生一次业务效果","2. Inbox：保证重复消息只产生一次业务效果",[14,4141,4142],{},"Inbox 位于消息消费方，用来记录已经处理过的事件，主要解决至少一次投递带来的重复消费问题。",[14,4144,4081],{},[135,4146,4149],{"className":4147,"code":4148,"language":140,"meta":141},[138],"收到 Kafka 消息\n→ 开启数据库事务\n→ 根据 eventId 写入 Inbox 记录\n→ 执行业务更新\n→ 提交数据库事务\n→ 提交 Kafka Offset\n",[77,4150,4148],{"__ignoreMap":141},[14,4152,4153,4154,4156],{},"Inbox 表通常对 ",[77,4155,4104],{}," 建立唯一索引。重复消息到达时，如果发现该事件已经存在，就不再重复执行业务逻辑。",[14,4158,4159],{},"Inbox 的关键点：",[21,4161,4162,4165,4168,4171],{},[24,4163,4164],{},"Inbox 记录和业务更新必须处于同一个数据库事务。",[24,4166,4167],{},"唯一索引负责防止同一事件被并发重复处理。",[24,4169,4170],{},"数据库事务提交后、Offset 提交前发生故障，消息仍会重投，但 Inbox 可以识别重复。",[24,4172,4173],{},"如果消费逻辑还会调用外部 HTTP 接口，Inbox 无法自动让数据库和外部接口形成原子事务；仍需下游幂等、重试或补偿。",[43,4175,4177],{"id":4176},"_3-两者的区别","3. 两者的区别",[230,4179,4180,4192],{},[233,4181,4182],{},[236,4183,4184,4186,4189],{},[239,4185,241],{},[239,4187,4188],{},"Outbox",[239,4190,4191],{},"Inbox",[249,4193,4194,4205,4216,4227,4238],{},[236,4195,4196,4199,4202],{},[254,4197,4198],{},"所在位置",[254,4200,4201],{},"消息生产方",[254,4203,4204],{},"消息消费方",[236,4206,4207,4210,4213],{},[254,4208,4209],{},"主要目标",[254,4211,4212],{},"防止业务成功但事件未发出",[254,4214,4215],{},"防止重复消息重复产生业务效果",[236,4217,4218,4221,4224],{},[254,4219,4220],{},"本地事务内容",[254,4222,4223],{},"业务更新 + 写事件表",[254,4225,4226],{},"写消费记录 + 业务更新",[236,4228,4229,4232,4235],{},[254,4230,4231],{},"后台动作",[254,4233,4234],{},"把待发送事件投递到 Kafka",[254,4236,4237],{},"通常不需要再次投递",[236,4239,4240,4243,4246],{},[254,4241,4242],{},"仍需注意",[254,4244,4245],{},"投递过程可能产生重复消息",[254,4247,4248],{},"外部调用仍需幂等或补偿",[14,4250,4251],{},"一句话区分：",[47,4253,4254],{},[14,4255,4256],{},[213,4257,4258],{},"Outbox 保证“该发的消息最终发出去”，Inbox 保证“重复收到的消息不会重复生效”。",[10,4260,4262],{"id":4261},"六三种投递语义","六、三种投递语义",[230,4264,4265,4278],{},[233,4266,4267],{},[236,4268,4269,4272,4275],{},[239,4270,4271],{},"语义",[239,4273,4274],{},"Offset 与业务处理顺序",[239,4276,4277],{},"结果",[249,4279,4280,4291,4302],{},[236,4281,4282,4285,4288],{},[254,4283,4284],{},"At Most Once",[254,4286,4287],{},"先提交，再处理",[254,4289,4290],{},"可能丢失，通常不重复",[236,4292,4293,4296,4299],{},[254,4294,4295],{},"At Least Once",[254,4297,4298],{},"先处理，再提交",[254,4300,4301],{},"不轻易丢失，可能重复",[236,4303,4304,4307,4310],{},[254,4305,4306],{},"Exactly Once",[254,4308,4309],{},"事务或幂等机制协调",[254,4311,4312],{},"业务效果恰好一次",[14,4314,4315],{},"绝大多数业务系统采用：",[47,4317,4318],{},[14,4319,4320],{},[213,4321,4322],{},"Kafka 至少一次投递 + 消费端业务幂等。",[14,4324,4325],{},"这通常比追求所有环节的强事务更容易实现，也更便于扩展。",[10,4327,4329],{"id":4328},"七常见故障与保障措施","七、常见故障与保障措施",[230,4331,4332,4345],{},[233,4333,4334],{},[236,4335,4336,4339,4342],{},[239,4337,4338],{},"故障场景",[239,4340,4341],{},"可能后果",[239,4343,4344],{},"主要保障",[249,4346,4347,4358,4371,4384,4396,4407,4418,4429],{},[236,4348,4349,4352,4355],{},[254,4350,4351],{},"生产者网络抖动",[254,4353,4354],{},"发送失败或结果未知",[254,4356,4357],{},"重试、幂等生产、回调处理",[236,4359,4360,4363,4366],{},[254,4361,4362],{},"Leader 写入后立即故障",[254,4364,4365],{},"未同步消息丢失",[254,4367,4368,4370],{},[77,4369,3445],{},"、副本、ISR",[236,4372,4373,4376,4379],{},[254,4374,4375],{},"ISR 只剩一个副本",[254,4377,4378],{},"单点故障风险升高",[254,4380,4381,4383],{},[77,4382,3606],{}," 拒绝写入",[236,4385,4386,4389,4392],{},[254,4387,4388],{},"同步副本全部不可用",[254,4390,4391],{},"非同步副本选主后日志回退",[254,4393,3864,4394],{},[77,4395,3453],{},[236,4397,4398,4401,4404],{},[254,4399,4400],{},"消费者先提交后处理",[254,4402,4403],{},"业务消息被跳过",[254,4405,4406],{},"关闭自动提交，成功后提交",[236,4408,4409,4412,4415],{},[254,4410,4411],{},"处理成功但提交前崩溃",[254,4413,4414],{},"重复消费",[254,4416,4417],{},"业务幂等、唯一流水号",[236,4419,4420,4423,4426],{},[254,4421,4422],{},"数据库成功、Kafka 失败",[254,4424,4425],{},"业务事件未发出",[254,4427,4428],{},"Transactional Outbox",[236,4430,4431,4434,4437],{},[254,4432,4433],{},"Kafka 成功、下游失败",[254,4435,4436],{},"业务结果缺失",[254,4438,4439],{},"重试、死信、补偿、告警",[10,4441,4443],{"id":4442},"八常见误区","八、常见误区",[43,4445,4447,4448,4450],{"id":4446},"误区一配置-acksall-就绝对不会丢","误区一：配置 ",[77,4449,3445],{}," 就绝对不会丢",[14,4452,4453,4455,4456,4458],{},[77,4454,3445],{}," 只解决生产者到 Broker 的确认强度，还要配合副本数、",[77,4457,3606],{},"、正确选主和消费者提交策略。",[43,4460,4462],{"id":4461},"误区二配置无限重试就不会丢","误区二：配置无限重试就不会丢",[14,4464,4465,4466,4468],{},"重试仍然受 ",[77,4467,3659],{}," 等条件限制，而且权限错误、序列化错误等问题无法靠盲目重试解决。最终失败必须被业务感知和补偿。",[43,4470,4472],{"id":4471},"误区三手动提交-offset-就是-exactly-once","误区三：手动提交 Offset 就是 Exactly Once",[14,4474,4475],{},"手动提交只能帮助建立“处理成功后提交”的顺序，崩溃窗口仍可能造成重复消费，因此还需要业务幂等。",[43,4477,4479],{"id":4478},"误区四kafka-事务可以自动覆盖数据库","误区四：Kafka 事务可以自动覆盖数据库",[14,4481,4482],{},"Kafka 事务主要协调 Kafka 内部的消息与 Offset，不会自动与 MySQL 等外部数据库形成同一个原子事务。",[43,4484,4486],{"id":4485},"误区五broker-返回成功就等于所有副本已经物理刷盘","误区五：Broker 返回成功就等于所有副本已经物理刷盘",[14,4488,4489],{},"Kafka 的确认、副本复制和磁盘刷盘是不同概念。高可靠主要依靠多副本和同步副本约束，而不是把每条消息都单独同步刷盘。",[10,4491,4493],{"id":4492},"九监控和运维同样重要","九、监控和运维同样重要",[14,4495,4496],{},"配置正确后，还应持续监控：",[21,4498,4499,4502,4505,4508,4511,4514],{},[24,4500,4501],{},"发送失败率、重试次数和发送延迟。",[24,4503,4504],{},"ISR 收缩、未充分复制分区和离线分区。",[24,4506,4507],{},"Broker 磁盘空间、磁盘延迟和网络异常。",[24,4509,4510],{},"消费积压、消费失败、重平衡次数。",[24,4512,4513],{},"死信消息和补偿任务堆积。",[24,4515,4516],{},"业务事件与最终结果的对账差异。",[14,4518,4519],{},"没有监控和对账，即使消息已经丢失，系统也可能长期无法发现。",[10,4521,1007],{"id":1007},[21,4523,4524,4531,4538,4545,4552],{},[24,4525,4526],{},[58,4527,4530],{"href":4528,"rel":4529},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fconfiguration\u002Fproducer-configs\u002F",[1016],"Apache Kafka Producer Configs",[24,4532,4533],{},[58,4534,4537],{"href":4535,"rel":4536},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fconfiguration\u002Fbroker-configs\u002F",[1016],"Apache Kafka Broker Configs",[24,4539,4540],{},[58,4541,4544],{"href":4542,"rel":4543},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fconfiguration\u002Fconsumer-configs\u002F",[1016],"Apache Kafka Consumer Configs",[24,4546,4547],{},[58,4548,4551],{"href":4549,"rel":4550},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fdesign\u002Fdesign\u002F",[1016],"Apache Kafka Design：Message Delivery Semantics",[24,4553,4554],{},[58,4555,4558],{"href":4556,"rel":4557},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fjavadoc\u002Forg\u002Fapache\u002Fkafka\u002Fclients\u002Fconsumer\u002FKafkaConsumer.html",[1016],"Apache KafkaConsumer JavaDoc",[1047,4560,1049],{},{"title":141,"searchDepth":504,"depth":504,"links":4562},[4563,4564,4565,4566,4573,4582,4588,4593,4594,4595,4603,4604],{"id":12,"depth":504,"text":12},{"id":54,"depth":504,"text":54},{"id":3491,"depth":504,"text":3492},{"id":3515,"depth":504,"text":3516,"children":4567},[4568,4570,4571,4572],{"id":3519,"depth":510,"text":4569},"1. 使用 acks=all",{"id":3610,"depth":510,"text":3611},{"id":3666,"depth":510,"text":3667},{"id":3731,"depth":510,"text":3732},{"id":3759,"depth":504,"text":3760,"children":4574},[4575,4576,4577,4579,4580],{"id":3763,"depth":510,"text":3764},{"id":3782,"depth":510,"text":3783},{"id":3812,"depth":510,"text":4578},"3. 配置 min.insync.replicas",{"id":3838,"depth":510,"text":3839},{"id":3878,"depth":510,"text":4581},"5. acks=all 不等于每条消息都立即 fsync",{"id":3896,"depth":504,"text":3897,"children":4583},[4584,4585,4586,4587],{"id":3900,"depth":510,"text":3901},{"id":3925,"depth":510,"text":3926},{"id":3990,"depth":510,"text":3991},{"id":4030,"depth":510,"text":4031},{"id":4061,"depth":504,"text":4062,"children":4589},[4590,4591,4592],{"id":4074,"depth":510,"text":4075},{"id":4138,"depth":510,"text":4139},{"id":4176,"depth":510,"text":4177},{"id":4261,"depth":504,"text":4262},{"id":4328,"depth":504,"text":4329},{"id":4442,"depth":504,"text":4443,"children":4596},[4597,4599,4600,4601,4602],{"id":4446,"depth":510,"text":4598},"误区一：配置 acks=all 就绝对不会丢",{"id":4461,"depth":510,"text":4462},{"id":4471,"depth":510,"text":4472},{"id":4478,"depth":510,"text":4479},{"id":4485,"depth":510,"text":4486},{"id":4492,"depth":504,"text":4493},{"id":1007,"depth":504,"text":1007},"wiki:java:kafka-message-loss-prevention","从生产者、Broker 和消费者三段链路分析 Kafka 消息丢失的原因，以及 acks、ISR、副本、手动提交 Offset 和业务幂等的完整保障方案。",{},72,"\u002Fjava\u002Fkafka-message-loss-prevention",{"title":3430,"description":4606},"java\u002Fkafka-message-loss-prevention","2026-07-24","mKpAptREGG2pnoZj7JmhkcDtGJ_YBax3G4LfJlMVoxY",{"id":4615,"title":4616,"body":4617,"commentId":5620,"description":5621,"difficulty":1082,"draft":1083,"extension":1084,"meta":5622,"navigation":1086,"order":5623,"path":5624,"section":1089,"seo":5625,"stem":5626,"updated":4612,"__hash__":5627},"java\u002Fjava\u002Fkafka-high-concurrency-performance-availability.md","Kafka 是如何实现高并发、高性能、高可用的",{"type":7,"value":4618,"toc":5577},[4619,4622,4624,4638,4640,4647,4649,4652,4666,4675,4679,4683,4686,4692,4695,4702,4706,4709,4712,4723,4726,4730,4733,4735,4741,4744,4748,4751,4754,4758,4762,4765,4771,4774,4778,4781,4787,4807,4810,4814,4817,4823,4826,4840,4846,4850,4853,4859,4862,4879,4886,4892,4895,4898,4905,4908,4928,4937,4941,4944,4947,4961,4978,4982,4985,4991,4997,5003,5006,5015,5021,5024,5031,5037,5041,5044,5047,5053,5055,5061,5064,5070,5073,5076,5079,5094,5097,5101,5105,5108,5114,5117,5120,5124,5127,5133,5136,5145,5148,5153,5156,5159,5163,5168,5179,5182,5196,5199,5202,5208,5211,5215,5222,5228,5231,5234,5282,5285,5291,5294,5298,5301,5304,5310,5313,5317,5379,5382,5388,5392,5395,5415,5418,5421,5427,5430,5434,5438,5441,5445,5451,5455,5458,5462,5465,5472,5479,5483,5486,5497,5501,5514,5517,5531,5533,5575],[2530,4620,4616],{"id":4621},"kafka-是如何实现高并发高性能高可用的",[10,4623,12],{"id":12},[47,4625,4626],{},[14,4627,4628,4629,4632,4633,220,4635,4637],{},"Kafka 的高并发主要依靠分区实现横向扩展：一个 Topic 被拆成多个 Partition，分区 Leader 分散在不同 Broker 上，生产者可以并行写入，消费者组也可以按分区并行消费。高性能主要来自追加写和顺序 I\u002FO、操作系统 Page Cache、生产与消费两端的批处理、批量压缩，以及通过 ",[77,4630,4631],{},"sendfile"," 实现的零拷贝传输。高可用则依靠多副本、Leader\u002FFollower、ISR、故障选主和 KRaft Controller Quorum；生产端再配合 ",[77,4634,3445],{},[77,4636,3606],{}," 和幂等生产，消费端通过 Consumer Group、Offset 与重新分配实现故障恢复。Kafka 的核心设计是在分区粒度上并行，在单分区内部维持有序日志，再用副本机制保障故障后的连续服务。",[14,4639,3457],{},[47,4641,4642],{},[14,4643,4644],{},[213,4645,4646],{},"分区提供并行度，顺序日志、批处理和零拷贝提供吞吐量，副本、ISR 与故障选主提供可用性。",[10,4648,54],{"id":54},[14,4650,4651],{},"Kafka 的三个能力不是彼此孤立的：",[21,4653,4654,4657,4660,4663],{},[24,4655,4656],{},"分区既是并发执行和横向扩展的单位，也是副本复制与故障恢复的单位。",[24,4658,4659],{},"批处理既减少网络请求，也把大量小写入转化为更高效的顺序写。",[24,4661,4662],{},"Page Cache 既提升读写性能，也能配合副本机制避免为了可靠性而对每条消息执行同步刷盘。",[24,4664,4665],{},"副本增强了可靠性，但同时会增加网络、磁盘与确认延迟，因此需要在吞吐、延迟和可靠性之间取舍。",[14,4667,4668],{},[58,4669,4672],{"href":4670,"target":61,"rel":4671},"\u002Fimages\u002Fwiki\u002Fjava\u002Fkafka-high-concurrency-performance-availability.svg",[63,64],[66,4673],{"src":4670,"alt":4674},"Kafka 高并发、高性能和高可用实现机制总览",[10,4676,4678],{"id":4677},"一高并发通过分区横向扩展","一、高并发：通过分区横向扩展",[43,4680,4682],{"id":4681},"_1-partition-是-kafka-的并行单元","1. Partition 是 Kafka 的并行单元",[14,4684,4685],{},"一个 Topic 可以拆成多个 Partition：",[135,4687,4690],{"className":4688,"code":4689,"language":140,"meta":141},[138],"Topic: order-events\n├── Partition 0 → Leader 在 Broker A\n├── Partition 1 → Leader 在 Broker B\n└── Partition 2 → Leader 在 Broker C\n",[77,4691,4689],{"__ignoreMap":141},[14,4693,4694],{},"不同分区可以由不同 Broker 同时处理，因此整体吞吐不再受限于单台机器。增加 Broker 并合理增加分区后，存储容量、网络带宽和读写能力都可以横向扩展。",[14,4696,4697,4698,4701],{},"Kafka 只保证",[213,4699,4700],{},"单分区内有序","，不保证一个 Topic 的所有分区之间全局有序。这是它获得并行能力的重要前提。",[43,4703,4705],{"id":4704},"_2-生产者直接找到分区-leader","2. 生产者直接找到分区 Leader",[14,4707,4708],{},"生产者会缓存集群元数据，根据消息 Key、显式分区或默认分区策略选择 Partition，然后直接向该分区的 Leader 发送数据，中间不需要额外的统一路由节点。",[14,4710,4711],{},"常见分区方式：",[21,4713,4714,4717,4720],{},[24,4715,4716],{},"指定 Key：相同 Key 通常进入同一分区，适合保证同一订单、用户或账户内的消息顺序。",[24,4718,4719],{},"不指定 Key：默认分区策略会在适当时机选择分区，并尽量形成较大的批次。",[24,4721,4722],{},"自定义分区器：按租户、地区或业务规则路由。",[14,4724,4725],{},"如果 Key 分布不均，大量消息集中到少数分区，就会出现热点分区。此时即使 Broker 很多，整体吞吐仍会被热点 Leader 限制。",[43,4727,4729],{"id":4728},"_3-consumer-group-按分区并行消费","3. Consumer Group 按分区并行消费",[14,4731,4732],{},"同一个 Consumer Group 中，一个 Partition 在同一时刻只分配给一个 Consumer；一个 Consumer 可以负责多个 Partition。",[14,4734,106],{},[135,4736,4739],{"className":4737,"code":4738,"language":140,"meta":141},[138],"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",[77,4740,4738],{"__ignoreMap":141},[14,4742,4743],{},"因此，一个消费组的有效并行度上限通常受 Partition 数量约束。简单增加 Consumer，而不增加 Partition，不一定能提高吞吐量。",[43,4745,4747],{"id":4746},"_4-broker-内部通过线程分工处理请求","4. Broker 内部通过线程分工处理请求",[14,4749,4750],{},"Broker 不会用一个线程串行完成所有网络和磁盘工作。网络线程负责接收请求、解析协议和返回响应，请求处理线程负责执行 Produce、Fetch 等具体逻辑，后台线程负责日志清理、副本同步等任务。",[14,4752,4753],{},"这种职责分离让网络连接管理、业务请求处理和后台维护能够并行执行，避免某类工作完全阻塞其他工作。",[10,4755,4757],{"id":4756},"二高性能把随机小操作变成顺序批量操作","二、高性能：把随机小操作变成顺序批量操作",[43,4759,4761],{"id":4760},"_1-顺序追加写","1. 顺序追加写",[14,4763,4764],{},"Kafka 的 Partition 本质上是一条只追加的日志。新消息写到日志末尾，不需要像通用数据库那样频繁进行随机位置更新。",[135,4766,4769],{"className":4767,"code":4768,"language":140,"meta":141},[138],"旧数据 ────────────────→ 日志末尾追加新批次\noffset 0  1  2  3  4  5  6 ...\n",[77,4770,4768],{"__ignoreMap":141},[14,4772,4773],{},"顺序 I\u002FO 能充分利用磁盘和操作系统的预读、合并写能力。日志达到一定大小或时间后会切分为新的 Segment，旧 Segment 可以按保留策略清理或压缩，不影响当前日志继续追加。",[43,4775,4777],{"id":4776},"_2-分段日志与稀疏索引","2. 分段日志与稀疏索引",[14,4779,4780],{},"每个 Partition 由多个日志 Segment 组成，常见文件包括：",[135,4782,4785],{"className":4783,"code":4784,"language":140,"meta":141},[138],"00000000000000000000.log\n00000000000000000000.index\n00000000000000000000.timeindex\n",[77,4786,4784],{"__ignoreMap":141},[21,4788,4789,4795,4801],{},[24,4790,4791,4794],{},[77,4792,4793],{},".log"," 保存消息批次。",[24,4796,4797,4800],{},[77,4798,4799],{},".index"," 保存相对 Offset 到日志物理位置的稀疏映射。",[24,4802,4803,4806],{},[77,4804,4805],{},".timeindex"," 保存时间戳到相对 Offset 的稀疏映射。",[14,4808,4809],{},"查找消息时，Kafka 先定位 Segment，再通过稀疏索引找到接近目标的位置，最后在日志中继续顺序查找。索引不需要记录每一条消息，因此能控制索引体积和内存占用。",[43,4811,4813],{"id":4812},"_3-充分利用-page-cache","3. 充分利用 Page Cache",[14,4815,4816],{},"Kafka 不把全部消息封装成大量 Java 堆对象长期缓存，而是依赖操作系统 Page Cache：",[135,4818,4821],{"className":4819,"code":4820,"language":140,"meta":141},[138],"Producer\n→ Broker 写日志文件\n→ 数据先进入 Page Cache\n→ 操作系统异步回写磁盘\n",[77,4822,4820],{"__ignoreMap":141},[14,4824,4825],{},"这样做有几个好处：",[21,4827,4828,4831,4834,4837],{},[24,4829,4830],{},"利用操作系统成熟的预读和回写机制。",[24,4832,4833],{},"减少 JVM 堆内存占用和 GC 压力。",[24,4835,4836],{},"热数据可以直接从 Page Cache 读取。",[24,4838,4839],{},"Broker 重启后，操作系统缓存仍可能保留部分热数据，不必完全重新构建 JVM 内缓存。",[14,4841,4842,4843,4845],{},"Kafka 的可靠性主要依赖多副本，而不是对每条消息都执行一次同步 ",[77,4844,3885],{},"。如果强制每条消息同步刷盘，吞吐和延迟都会明显恶化。",[43,4847,4849],{"id":4848},"_4-端到端批处理","4. 端到端批处理",[14,4851,4852],{},"批处理贯穿生产、Broker 存储和消费三段链路：",[135,4854,4857],{"className":4855,"code":4856,"language":140,"meta":141},[138],"多条消息\n→ Producer 聚合成 Record Batch\n→ 一次网络请求发送\n→ Broker 将 Record Batch 连续追加到分区日志\n→ Consumer 一次 Fetch 多条消息\n",[77,4858,4856],{"__ignoreMap":141},[14,4860,4861],{},"它可以摊薄：",[21,4863,4864,4867,4870,4873,4876],{},[24,4865,4866],{},"网络往返成本。",[24,4868,4869],{},"系统调用成本。",[24,4871,4872],{},"协议头和请求头开销。",[24,4874,4875],{},"磁盘写入次数。",[24,4877,4878],{},"消息压缩成本。",[14,4880,4881,4882,4885],{},"这里的“Broker 追加批次”不是把多条消息合并成一条业务消息，也不承诺底层一定只发生一次系统调用。它表示 Kafka 的生产者和存储格式都以 ",[213,4883,4884],{},"Record Batch"," 为基本单位：",[135,4887,4890],{"className":4888,"code":4889,"language":140,"meta":141},[138],"Record Batch\n├── 批次头：baseOffset、长度、时间戳、压缩类型、CRC 等\n├── Record 1\n├── Record 2\n└── Record 3\n",[77,4891,4889],{"__ignoreMap":141},[14,4893,4894],{},"Producer 会先按 Partition 在内存中积累消息，形成 Record Batch。Broker 收到后，对批次进行校验，为其中的消息确定 Offset，然后把整段连续数据追加到对应 Partition 的当前日志 Segment 末尾。相比每条消息都单独发请求、单独写日志，批次追加可以用更少的请求和更大的连续 I\u002FO 摊薄固定开销。",[14,4896,4897],{},"一个 ProduceRequest 还可以携带多个 Partition 的 Record Batch，但 Broker 最终仍然分别追加到各个 Partition 的日志中。因此：",[47,4899,4900],{},[14,4901,4902],{},[213,4903,4904],{},"批次是网络传输和日志写入的基本单位，Partition 才是日志组织、顺序保证和副本复制的基本单位。",[14,4906,4907],{},"生产者常见相关参数：",[135,4909,4911],{"className":3590,"code":4910,"language":3592,"meta":141,"style":141},"batch.size=16384\nlinger.ms=5\ncompression.type=lz4\n",[77,4912,4913,4918,4923],{"__ignoreMap":141},[495,4914,4915],{"class":497,"line":498},[495,4916,4917],{},"batch.size=16384\n",[495,4919,4920],{"class":497,"line":504},[495,4921,4922],{},"linger.ms=5\n",[495,4924,4925],{"class":497,"line":510},[495,4926,4927],{},"compression.type=lz4\n",[14,4929,4930,1955,4933,4936],{},[77,4931,4932],{},"batch.size",[77,4934,4935],{},"linger.ms"," 体现的是吞吐与延迟之间的取舍：适度等待可以积累更大的批次，但等待过长会增加低流量场景的发送延迟。参数值应根据消息大小、流量和延迟目标压测确定，不能机械照搬示例。",[43,4938,4940],{"id":4939},"_5-批量压缩","5. 批量压缩",[14,4942,4943],{},"Kafka 对一个 Record Batch 进行整体压缩，而不是分别压缩每条消息。相似消息中的字段名和公共内容可以获得更好的压缩率。",[14,4945,4946],{},"压缩后的批次会以压缩形式写入日志并传输给消费者，从而降低：",[21,4948,4949,4952,4955,4958],{},[24,4950,4951],{},"Broker 磁盘占用。",[24,4953,4954],{},"生产者到 Broker 的网络流量。",[24,4956,4957],{},"副本复制流量。",[24,4959,4960],{},"Broker 到消费者的网络流量。",[14,4962,4963,4964,220,4967,220,4970,4973,4974,4977],{},"压缩会消耗 CPU，因此需要根据资源瓶颈选择 ",[77,4965,4966],{},"lz4",[77,4968,4969],{},"zstd",[77,4971,4972],{},"snappy"," 或 ",[77,4975,4976],{},"gzip"," 等算法。网络或磁盘是瓶颈时，压缩往往能换取更高吞吐。",[43,4979,4981],{"id":4980},"_6-零拷贝","6. 零拷贝",[14,4983,4984],{},"消费者拉取日志数据时，普通路径可能需要：",[135,4986,4989],{"className":4987,"code":4988,"language":140,"meta":141},[138],"磁盘 → Page Cache → 用户空间 → Socket Buffer → 网卡\n",[77,4990,4988],{"__ignoreMap":141},[14,4992,4993,4994,4996],{},"Kafka 在适用条件下利用 Linux ",[77,4995,4631],{},"，让数据从 Page Cache 直接进入网络发送路径，减少用户态和内核态之间的复制与上下文切换：",[135,4998,5001],{"className":4999,"code":5000,"language":140,"meta":141},[138],"Page Cache → Socket \u002F 网卡\n",[77,5002,5000],{"__ignoreMap":141},[14,5004,5005],{},"这就是常说的零拷贝。它主要优化 Broker 向消费者传输已有日志数据的过程。",[14,5007,5008],{},[58,5009,5012],{"href":5010,"target":61,"rel":5011},"\u002Fimages\u002Fwiki\u002Fjava\u002Fkafka-sendfile-vs-traditional-io.svg",[63,64],[66,5013],{"src":5010,"alt":5014},"Kafka sendfile 零拷贝与传统文件传输路径对比",[14,5016,5017,5018,5020],{},"传统路径需要先把数据从内核 Page Cache 复制到 Broker 的用户空间，再从用户空间写回内核 Socket Buffer；",[77,5019,4631],{}," 让内核直接组织 Page Cache 到网络 Socket 的传输，Broker 不再把消息内容完整搬入 JVM 用户空间。",[14,5022,5023],{},"所以“零拷贝”更准确的含义是：",[47,5025,5026],{},[14,5027,5028],{},[213,5029,5030],{},"减少或绕过 Page Cache 与用户空间之间不必要的数据复制，而不是数据在磁盘、内存和网卡之间一次都不移动。",[14,5032,5033,5034,5036],{},"需要注意，Kafka 官方文档明确说明，启用 SSL 时数据要经过用户态 TLS 处理，当前不会使用这条 ",[77,5035,4631],{}," 路径。因此不能笼统地说所有 Kafka 网络传输都一定是零拷贝。",[43,5038,5040],{"id":5039},"_7-consumer-pull-与长轮询","7. Consumer Pull 与长轮询",[14,5042,5043],{},"Kafka 由消费者主动 Pull 数据。消费者可以根据自己的处理能力决定拉取速度，处理不过来时，未消费消息会暂时保留在 Kafka 中，并表现为 Consumer Lag 增长，而不是由 Broker 持续 Push 直至压垮消费者。",[14,5045,5046],{},"Consumer Lag 表示消费者的消费进度落后于 Partition 最新位置的程度。监控消费组时，通常可以近似理解为：",[135,5048,5051],{"className":5049,"code":5050,"language":140,"meta":141},[138],"Consumer Lag\n= Partition Log End Offset\n- Consumer Group 已提交 Offset\n",[77,5052,5050],{"__ignoreMap":141},[14,5054,106],{},[135,5056,5059],{"className":5057,"code":5058,"language":140,"meta":141},[138],"Partition 下一条待写位置：10000\n消费组下一条待消费位置：9400\n\nLag = 10000 - 9400 = 600\n",[77,5060,5058],{"__ignoreMap":141},[14,5062,5063],{},"这表示当前大约还有 600 个 Offset 的数据尚未被该消费组处理。Lag 不是消息丢失，而是消费积压：",[135,5065,5068],{"className":5066,"code":5067,"language":140,"meta":141},[138],"生产速度 > 消费速度\n→ Lag 持续增大\n\n消费速度 > 生产速度\n→ Consumer 逐步追赶\n→ Lag 逐步减小\n",[77,5069,5067],{"__ignoreMap":141},[14,5071,5072],{},"Lag 不能只看某一时刻的绝对值，还要观察增长趋势、积压持续时间和业务允许的最大延迟。同样数量的 Lag，在每秒几条和每秒几十万条的 Topic 中代表的时间延迟完全不同。",[14,5074,5075],{},"如果 Lag 长期增长，可能是消费者处理变慢、下游故障、分区热点、Consumer 数量不足或频繁重平衡。若积压时间超过 Topic 的数据保留期限，旧消息可能已经被清理，消费者就不能再依靠 Kafka 把这部分数据完整追回。",[14,5077,5078],{},"Fetch 请求支持批量拉取和长轮询：",[135,5080,5082],{"className":3590,"code":5081,"language":3592,"meta":141,"style":141},"fetch.min.bytes=1\nfetch.max.wait.ms=500\n",[77,5083,5084,5089],{"__ignoreMap":141},[495,5085,5086],{"class":497,"line":498},[495,5087,5088],{},"fetch.min.bytes=1\n",[495,5090,5091],{"class":497,"line":504},[495,5092,5093],{},"fetch.max.wait.ms=500\n",[14,5095,5096],{},"Broker 暂时没有足够数据时，可以等待一段时间再响应，避免消费者无数据时频繁空轮询；流量较高时，又可以一次返回较大的批次。",[10,5098,5100],{"id":5099},"三高可用副本isr-与故障转移","三、高可用：副本、ISR 与故障转移",[43,5102,5104],{"id":5103},"_1-每个-partition-可以有多个副本","1. 每个 Partition 可以有多个副本",[14,5106,5107],{},"假设副本因子为 3：",[135,5109,5112],{"className":5110,"code":5111,"language":140,"meta":141},[138],"Partition 0\n├── Leader：Broker A\n├── Follower：Broker B\n└── Follower：Broker C\n",[77,5113,5111],{"__ignoreMap":141},[14,5115,5116],{},"生产和普通消费请求由 Leader 处理，Follower 分别从 Leader 拉取并复制日志。多个 Follower 的同步是彼此独立的，不存在先同步 B、再由 B 同步 C 的固定顺序。",[14,5118,5119],{},"副本应尽量分散在不同 Broker；具备机架感知配置时，还可以跨机架放置，降低单机或单机架故障带来的影响。",[43,5121,5123],{"id":5122},"_2-isr-维护可安全选主的副本集合","2. ISR 维护可安全选主的副本集合",[14,5125,5126],{},"ISR 是 In-Sync Replicas，即当前与 Leader 保持同步的副本集合。Follower 故障或长时间落后时会被移出 ISR，重新追上后可以再次加入。",[135,5128,5131],{"className":5129,"code":5130,"language":140,"meta":141},[138],"正常：ISR = [A, B, C]\nC 落后：ISR = [A, B]\n",[77,5132,5130],{"__ignoreMap":141},[14,5134,5135],{},"Leader 故障后，Controller 会从符合条件的副本中选择新的 Leader。以 ISR 为基础进行选主，可以避免把明显缺少最新日志的副本直接作为新的数据源。",[43,5137,5139,5140,5142,5143],{"id":5138},"_3-acks-与-mininsyncreplicas","3. ",[77,5141,3528],{}," 与 ",[77,5144,3606],{},[14,5146,5147],{},"常见的高可靠组合是：",[135,5149,5151],{"className":5150,"code":3822,"language":140,"meta":141},[138],[77,5152,3822],{"__ignoreMap":141},[14,5154,5155],{},"含义是生产者等待当前 ISR 的确认，并且 ISR 至少要有两个副本，否则 Broker 拒绝写入。",[14,5157,5158],{},"这个配置体现了 CAP 取舍：副本不足时拒绝写入，会降低部分可用性，但可以避免在只剩单副本时继续写入并承担更高的数据丢失风险。",[43,5160,5162],{"id":5161},"_4-kraft-controller-quorum-保证控制面可用","4. KRaft Controller Quorum 保证控制面可用",[5164,5165,5167],"h4",{"id":5166},"kraft-controller-quorum-是什么","KRaft Controller Quorum 是什么",[14,5169,5170,5171,5174,5175,5178],{},"KRaft 是 Kafka 自己实现的元数据管理机制，用来替代早期依赖的 ZooKeeper。集群中会有若干个承担 ",[77,5172,5173],{},"controller"," 角色的节点，这些 Controller 组成一个基于 Raft 共识协议的 ",[213,5176,5177],{},"Controller Quorum","，即控制器仲裁组。",[14,5180,5181],{},"它们维护的是集群元数据，而不是普通 Topic 的业务消息：",[21,5183,5184,5187,5190,5193],{},[24,5185,5186],{},"Broker 注册、存活状态和节点信息。",[24,5188,5189],{},"Topic 与 Partition 的定义。",[24,5191,5192],{},"每个 Partition 的副本分配、Leader 和 ISR。",[24,5194,5195],{},"配置、配额以及其他集群级元数据。",[14,5197,5198],{},"这些变化会写入 KRaft 的集群元数据日志，并复制给 Quorum 中的其他 Controller。一次元数据变更获得多数 Controller 确认后才算提交，因此单个 Controller 故障不会导致已提交元数据丢失。",[14,5200,5201],{},"常见部署是 3 个或 5 个 Controller：",[135,5203,5206],{"className":5204,"code":5205,"language":140,"meta":141},[138],"3 个 Controller → 多数派为 2，可容忍 1 个故障\n5 个 Controller → 多数派为 3，可容忍 2 个故障\n",[77,5207,5205],{"__ignoreMap":141},[14,5209,5210],{},"如果存活 Controller 失去多数派，集群不能安全提交新的元数据变更。此时已有数据读写是否还能短暂继续，取决于具体操作和现有元数据状态，但创建 Topic、Partition Leader 调度等控制面操作会受到影响。",[5164,5212,5214],{"id":5213},"active-controller-是什么","Active Controller 是什么",[14,5216,5217,5218,5221],{},"Controller Quorum 在同一时刻会选出一个 Leader，Kafka 的文档和运行语境中通常称它为 ",[213,5219,5220],{},"Active Controller","。其他 Controller 作为 Follower 复制元数据日志，随时准备接管。",[135,5223,5226],{"className":5224,"code":5225,"language":140,"meta":141},[138],"Controller A：Active Controller\n├── 接收和处理元数据变更\n├── 管理 Broker 注册与故障\n├── 触发 Partition Leader 选举\n└── 把变更写入元数据日志\n\nController B、C：Follower Controllers\n├── 复制元数据日志\n└── Active Controller 故障后参与重新选举\n",[77,5227,5225],{"__ignoreMap":141},[14,5229,5230],{},"Active Controller 不是所有业务消息的入口，也不负责替 Broker 保存普通 Topic 数据。生产者和消费者仍然直接与 Partition Leader 所在的 Broker 通信。",[14,5232,5233],{},"需要明确区分两个层面：",[230,5235,5236,5252],{},[233,5237,5238],{},[236,5239,5240,5243,5246,5249],{},[239,5241,5242],{},"层面",[239,5244,5245],{},"主要角色",[239,5247,5248],{},"保存什么",[239,5250,5251],{},"负责什么",[249,5253,5254,5268],{},[236,5255,5256,5259,5262,5265],{},[254,5257,5258],{},"数据面",[254,5260,5261],{},"Broker、Partition Leader\u002FFollower",[254,5263,5264],{},"普通 Topic 的业务消息",[254,5266,5267],{},"生产、消费、副本复制",[236,5269,5270,5273,5276,5279],{},[254,5271,5272],{},"控制面",[254,5274,5275],{},"KRaft Controller Quorum",[254,5277,5278],{},"集群元数据日志",[254,5280,5281],{},"节点管理、元数据变更、Leader 调度",[14,5283,5284],{},"例如 Broker A 突然故障：",[135,5286,5289],{"className":5287,"code":5288,"language":140,"meta":141},[138],"Active Controller 感知 Broker A 失联\n→ 找出受影响的 Partition\n→ 从符合条件的副本中选择新 Leader\n→ 将选举结果写入元数据日志\n→ 多数 Controller 确认提交\n→ 向相关 Broker 发布新元数据\n→ 客户端刷新元数据并访问新 Leader\n",[77,5290,5288],{"__ignoreMap":141},[14,5292,5293],{},"如果 Active Controller 自身故障，剩余 Controller 会通过 Raft 重新选出新的 Active Controller。新节点已经复制了已提交的元数据日志，因此可以继续管理集群。",[43,5295,5297],{"id":5296},"_5-consumer-group-自动接管故障消费者的分区","5. Consumer Group 自动接管故障消费者的分区",[14,5299,5300],{},"消费者通过心跳维持组成员身份。如果某个 Consumer 崩溃或超时，Consumer Group 会重新分配它原来负责的 Partition，让其他 Consumer 接管。",[14,5302,5303],{},"消费进度保存在已提交的 Offset 中，因此新 Consumer 可以从相应位置继续处理：",[135,5305,5308],{"className":5306,"code":5307,"language":140,"meta":141},[138],"Consumer A 故障\n→ 触发重新分配\n→ Partition 交给 Consumer B\n→ B 从已提交 Offset 继续消费\n",[77,5309,5307],{"__ignoreMap":141},[14,5311,5312],{},"重平衡期间可能产生短暂停顿，所以高可用不等于无感切换。合理设置会话、心跳、处理时长，并避免频繁扩缩容，有助于减少重平衡影响。",[10,5314,5316],{"id":5315},"四三种能力如何协同","四、三种能力如何协同",[230,5318,5319,5335],{},[233,5320,5321],{},[236,5322,5323,5326,5329,5332],{},[239,5324,5325],{},"目标",[239,5327,5328],{},"核心机制",[239,5330,5331],{},"解决的问题",[239,5333,5334],{},"主要代价",[249,5336,5337,5351,5365],{},[236,5338,5339,5342,5345,5348],{},[254,5340,5341],{},"高并发",[254,5343,5344],{},"多 Partition、多 Broker、Consumer Group",[254,5346,5347],{},"并行生产、存储和消费",[254,5349,5350],{},"分区与元数据管理成本",[236,5352,5353,5356,5359,5362],{},[254,5354,5355],{},"高性能",[254,5357,5358],{},"顺序写、Page Cache、批处理、压缩、零拷贝",[254,5360,5361],{},"降低 I\u002FO、复制和网络开销",[254,5363,5364],{},"批处理延迟与 CPU 消耗",[236,5366,5367,5370,5373,5376],{},[254,5368,5369],{},"高可用",[254,5371,5372],{},"多副本、ISR、Controller Quorum、故障转移",[254,5374,5375],{},"节点故障后继续提供服务",[254,5377,5378],{},"副本存储、网络与确认延迟",[14,5380,5381],{},"可以把整个链路理解为：",[135,5383,5386],{"className":5384,"code":5385,"language":140,"meta":141},[138],"消息按 Partition 分流\n→ Producer 批量发送\n→ Leader 顺序追加到日志\n→ Follower 并行复制\n→ 满足确认条件后返回\n→ Consumer Group 按 Partition 批量拉取\n",[77,5387,5385],{"__ignoreMap":141},[10,5389,5391],{"id":5390},"五为什么不能只靠增加分区提升性能","五、为什么不能只靠增加分区提升性能",[14,5393,5394],{},"分区是扩展能力的基础，但分区不是越多越好。分区数量增加会同时带来：",[21,5396,5397,5400,5403,5406,5409,5412],{},[24,5398,5399],{},"更多日志目录、Segment 和文件句柄。",[24,5401,5402],{},"更多副本复制任务。",[24,5404,5405],{},"更大的集群元数据。",[24,5407,5408],{},"更高的 Leader 选举和故障恢复成本。",[24,5410,5411],{},"Consumer Group 重平衡开销。",[24,5413,5414],{},"单 Key 顺序范围更难调整。",[14,5416,5417],{},"合理做法是根据目标吞吐量、单分区压测能力、消费者并行度、副本数和未来增长空间估算，而不是一次创建大量分区。",[14,5419,5420],{},"一个简化估算思路：",[135,5422,5425],{"className":5423,"code":5424,"language":140,"meta":141},[138],"分区数 ≥ max(\n  目标生产吞吐 \u002F 单分区生产吞吐,\n  目标消费吞吐 \u002F 单消费者单分区吞吐,\n  期望消费并行度\n)\n",[77,5426,5424],{"__ignoreMap":141},[14,5428,5429],{},"最终仍需要在接近生产环境的机器、网络、副本配置和消息大小下进行压测。",[10,5431,5433],{"id":5432},"六常见误区","六、常见误区",[43,5435,5437],{"id":5436},"误区一kafka-使用磁盘所以一定比内存消息队列慢","误区一：Kafka 使用磁盘，所以一定比内存消息队列慢",[14,5439,5440],{},"Kafka 采用顺序追加、Page Cache 和批处理，大量读写实际在操作系统缓存中完成。性能取决于访问模式，而不是简单由“使用磁盘”决定。",[43,5442,5444],{"id":5443},"误区二零拷贝让数据完全不发生复制","误区二：零拷贝让数据完全不发生复制",[14,5446,5447,5448,5450],{},"零拷贝是减少不必要的用户态复制，并非物理意义上的一次复制都没有；而且启用 SSL 时不会走 Kafka 文档描述的 ",[77,5449,4631],{}," 路径。",[43,5452,5454],{"id":5453},"误区三消费者越多消费速度一定越快","误区三：消费者越多，消费速度一定越快",[14,5456,5457],{},"同一消费组内，一个 Partition 同时只能交给一个 Consumer。Consumer 数超过 Partition 数后，多出的 Consumer 不会获得分区。",[43,5459,5461],{"id":5460},"误区四副本越多高可用越高且没有代价","误区四：副本越多，高可用越高且没有代价",[14,5463,5464],{},"更多副本会增加存储、网络复制和写确认成本。副本数应根据故障域和可靠性目标确定，常见三副本不代表所有场景都必须相同。",[43,5466,5468,5469,5471],{"id":5467},"误区五acksall-就代表所有配置副本都已写入","误区五：",[77,5470,3445],{}," 就代表所有配置副本都已写入",[14,5473,5474,5476,5477,401],{},[77,5475,3445],{}," 等待的是当前 ISR，而不是所有配置副本。要避免 ISR 只剩一个副本时仍然成功写入，需要配合 ",[77,5478,3606],{},[10,5480,5482],{"id":5481},"七排查性能和可用性问题时看什么","七、排查性能和可用性问题时看什么",[43,5484,5485],{"id":5485},"生产端",[21,5487,5488,5491,5494],{},[24,5489,5490],{},"发送吞吐、请求延迟、错误率和重试次数。",[24,5492,5493],{},"批次大小、压缩率和缓冲区等待。",[24,5495,5496],{},"消息 Key 是否造成分区流量倾斜。",[43,5498,5500],{"id":5499},"broker","Broker",[21,5502,5503,5506,5509,5511],{},[24,5504,5505],{},"各 Broker、Partition 和 Leader 的流量是否均衡。",[24,5507,5508],{},"磁盘利用率、磁盘延迟、网络带宽和请求队列。",[24,5510,4504],{},[24,5512,5513],{},"Page Cache 命中效果和系统是否发生 Swap。",[43,5515,5516],{"id":5516},"消费端",[21,5518,5519,5522,5525,5528],{},[24,5520,5521],{},"Consumer Lag 及其增长速度。",[24,5523,5524],{},"单条消息处理耗时和批次处理耗时。",[24,5526,5527],{},"Consumer 数量与 Partition 数量是否匹配。",[24,5529,5530],{},"重平衡次数、心跳超时和 Offset 提交失败。",[10,5532,1007],{"id":1007},[21,5534,5535,5541,5548,5555,5560,5565,5570],{},[24,5536,5537],{},[58,5538,5540],{"href":4549,"rel":5539},[1016],"Apache Kafka Design",[24,5542,5543],{},[58,5544,5547],{"href":5545,"rel":5546},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Foperations\u002Fkraft\u002F",[1016],"Apache Kafka KRaft",[24,5549,5550],{},[58,5551,5554],{"href":5552,"rel":5553},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fimplementation\u002Fmessage-format\u002F",[1016],"Apache Kafka Message Format",[24,5556,5557],{},[58,5558,4530],{"href":4528,"rel":5559},[1016],[24,5561,5562],{},[58,5563,4544],{"href":4542,"rel":5564},[1016],[24,5566,5567],{},[58,5568,4537],{"href":4535,"rel":5569},[1016],[24,5571,5572],{},[58,5573,4558],{"href":4556,"rel":5574},[1016],[1047,5576,1049],{},{"title":141,"searchDepth":504,"depth":504,"links":5578},[5579,5580,5581,5587,5596,5604,5605,5606,5614,5619],{"id":12,"depth":504,"text":12},{"id":54,"depth":504,"text":54},{"id":4677,"depth":504,"text":4678,"children":5582},[5583,5584,5585,5586],{"id":4681,"depth":510,"text":4682},{"id":4704,"depth":510,"text":4705},{"id":4728,"depth":510,"text":4729},{"id":4746,"depth":510,"text":4747},{"id":4756,"depth":504,"text":4757,"children":5588},[5589,5590,5591,5592,5593,5594,5595],{"id":4760,"depth":510,"text":4761},{"id":4776,"depth":510,"text":4777},{"id":4812,"depth":510,"text":4813},{"id":4848,"depth":510,"text":4849},{"id":4939,"depth":510,"text":4940},{"id":4980,"depth":510,"text":4981},{"id":5039,"depth":510,"text":5040},{"id":5099,"depth":504,"text":5100,"children":5597},[5598,5599,5600,5602,5603],{"id":5103,"depth":510,"text":5104},{"id":5122,"depth":510,"text":5123},{"id":5138,"depth":510,"text":5601},"3. acks 与 min.insync.replicas",{"id":5161,"depth":510,"text":5162},{"id":5296,"depth":510,"text":5297},{"id":5315,"depth":504,"text":5316},{"id":5390,"depth":504,"text":5391},{"id":5432,"depth":504,"text":5433,"children":5607},[5608,5609,5610,5611,5612],{"id":5436,"depth":510,"text":5437},{"id":5443,"depth":510,"text":5444},{"id":5453,"depth":510,"text":5454},{"id":5460,"depth":510,"text":5461},{"id":5467,"depth":510,"text":5613},"误区五：acks=all 就代表所有配置副本都已写入",{"id":5481,"depth":504,"text":5482,"children":5615},[5616,5617,5618],{"id":5485,"depth":510,"text":5485},{"id":5499,"depth":510,"text":5500},{"id":5516,"depth":510,"text":5516},{"id":1007,"depth":504,"text":1007},"wiki:java:kafka-high-concurrency-performance-availability","从分区并行、顺序写、Page Cache、批处理、零拷贝、副本与 ISR 等机制，系统理解 Kafka 高吞吐与高可用架构。",{},73,"\u002Fjava\u002Fkafka-high-concurrency-performance-availability",{"title":4616,"description":5621},"java\u002Fkafka-high-concurrency-performance-availability","0TlKAnCJWs7ZZl90WzviZ4k1DZq74xMTxxLN4sHf-c8",{"id":5629,"title":5630,"body":5631,"commentId":6961,"description":6962,"difficulty":1082,"draft":1083,"extension":1084,"meta":6963,"navigation":1086,"order":6964,"path":6965,"section":1089,"seo":6966,"stem":6967,"updated":4612,"__hash__":6968},"java\u002Fjava\u002Fkafka-message-backlog-handling.md","Kafka 消息积压如何处理",{"type":7,"value":5632,"toc":6916},[5633,5636,5638,5643,5645,5652,5654,5657,5663,5666,5675,5679,5682,5685,5690,5692,5698,5701,5704,5775,5778,5781,5787,5790,5796,5799,5802,5806,5810,5816,5819,5823,5826,5849,5856,5860,5863,5883,5886,5892,5895,5899,5902,5905,5911,5914,5918,5921,5938,5941,5945,5949,5952,5958,5961,5965,5968,5982,5985,5988,5994,5997,6001,6004,6007,6010,6014,6018,6021,6024,6030,6033,6037,6040,6063,6065,6071,6076,6079,6085,6088,6091,6108,6111,6115,6118,6146,6149,6177,6181,6184,6190,6193,6199,6202,6212,6215,6221,6224,6232,6246,6255,6259,6272,6275,6281,6285,6288,6294,6297,6303,6306,6309,6312,6315,6318,6349,6352,6356,6360,6363,6366,6373,6376,6380,6383,6386,6392,6395,6401,6404,6407,6412,6415,6418,6422,6428,6431,6435,6441,6447,6450,6453,6467,6471,6474,6480,6483,6487,6490,6493,6499,6502,6522,6525,6529,6532,6538,6541,6555,6558,6562,6565,6571,6574,6580,6583,6589,6591,6597,6600,6606,6610,6613,6620,6623,6640,6643,6647,6650,6653,6713,6720,6773,6776,6779,6785,6791,6794,6797,6803,6806,6810,6813,6836,6840,6844,6847,6854,6857,6861,6864,6868,6875,6878,6882,6885,6887,6913],[2530,5634,5630],{"id":5635},"kafka-消息积压如何处理",[10,5637,12],{"id":12},[47,5639,5640],{},[14,5641,5642],{},"Kafka 消息积压不能一上来就盲目增加消费者，首先要确认积压范围和原因：观察各 Partition 的 Consumer Lag、生产速率、消费速率、消费者存活状态、重平衡、处理耗时和下游依赖。如果消费实例异常或下游故障，应先恢复服务；如果生产速度持续大于消费速度，需要对非核心生产流量限流，同时扩容消费者，但同一消费组的有效并行度不会超过 Partition 数量。如果单条处理过慢，应通过批量写库、减少同步 RPC、优化慢 SQL 或使用受控的异步处理提高单实例吞吐；如果只有个别 Partition 积压，要检查消息 Key 是否造成热点；如果被异常消息反复阻塞，需要有限重试后进入死信或人工补偿。恢复时间可以用“积压量 ÷（消费速率－生产速率）”估算，只有消费能力大于生产速度，积压才会真正下降。Offset 跳到最新只能在业务明确同意丢弃旧消息时使用，不能作为常规解决方案。",[14,5644,3457],{},[47,5646,5647],{},[14,5648,5649],{},[213,5650,5651],{},"先判断为什么消费速度落后，再从减少流入、恢复异常、提升单实例吞吐和增加有效并行度四个方向处理。",[10,5653,54],{"id":54},[14,5655,5656],{},"Kafka 消息积压的本质是：",[135,5658,5661],{"className":5659,"code":5660,"language":140,"meta":141},[138],"一段时间内的生产速度\n>\n一段时间内的有效消费速度\n",[77,5662,5660],{"__ignoreMap":141},[14,5664,5665],{},"积压不是单一故障类型，它可能由突发流量、消费者异常、下游变慢、分区热点或异常消息阻塞引起。不同原因的处理方式完全不同。",[14,5667,5668],{},[58,5669,5672],{"href":5670,"target":61,"rel":5671},"\u002Fimages\u002Fwiki\u002Fjava\u002Fkafka-message-backlog-handling.svg",[63,64],[66,5673],{"src":5670,"alt":5674},"Kafka 消息积压排查与处理流程",[10,5676,5678],{"id":5677},"一什么是-kafka-消息积压","一、什么是 Kafka 消息积压",[14,5680,5681],{},"每个 Partition 中的消息都有 Offset。生产者持续向日志末尾写入，消费组通过已提交 Offset 记录自己的处理进度。",[14,5683,5684],{},"监控消费组时，Consumer Lag 通常可以近似理解为：",[135,5686,5688],{"className":5687,"code":5050,"language":140,"meta":141},[138],[77,5689,5050],{"__ignoreMap":141},[14,5691,106],{},[135,5693,5696],{"className":5694,"code":5695,"language":140,"meta":141},[138],"Partition Log End Offset：120000\n消费组已提交 Offset：95000\n\nLag = 120000 - 95000 = 25000\n",[77,5697,5695],{"__ignoreMap":141},[14,5699,5700],{},"表示这个消费组在该 Partition 上大约落后 25000 个 Offset。",[14,5702,5703],{},"需要同时观察三类指标：",[230,5705,5706,5718],{},[233,5707,5708],{},[236,5709,5710,5713,5715],{},[239,5711,5712],{},"指标",[239,5714,1486],{},[239,5716,5717],{},"判断价值",[249,5719,5720,5731,5742,5753,5764],{},[236,5721,5722,5725,5728],{},[254,5723,5724],{},"当前 Lag",[254,5726,5727],{},"还落后多少 Offset",[254,5729,5730],{},"反映积压规模",[236,5732,5733,5736,5739],{},[254,5734,5735],{},"Lag 增长速度",[254,5737,5738],{},"单位时间新增多少积压",[254,5740,5741],{},"判断问题是否正在恶化",[236,5743,5744,5747,5750],{},[254,5745,5746],{},"最老未处理消息年龄",[254,5748,5749],{},"最老待处理消息的时间戳距现在多久",[254,5751,5752],{},"反映业务已经延迟多久",[236,5754,5755,5758,5761],{},[254,5756,5757],{},"积压字节量",[254,5759,5760],{},"待处理消息占用的总字节数",[254,5762,5763],{},"反映网络、磁盘和反序列化压力",[236,5765,5766,5769,5772],{},[254,5767,5768],{},"单条处理耗时",[254,5770,5771],{},"处理一条或一批消息实际需要多久",[254,5773,5774],{},"用于估算积压清空速度",[14,5776,5777],{},"只看 Lag 绝对值容易误判。例如同样积压 10 万条，在每秒消费 20 万条的系统里可能很快恢复，在每秒只消费 100 条的系统里则非常严重。",[14,5779,5780],{},"Lag 是 Offset 数量差，本身只表示“落后多少条记录”，不能直接等同于时间延迟。对于相同的 10 万 Lag，消费者处理速度越快，处理完现有积压所需的时间越短：",[135,5782,5785],{"className":5783,"code":5784,"language":140,"meta":141},[138],"处理现有积压所需时间\n≈ Lag ÷ 消费速度\n",[77,5786,5784],{"__ignoreMap":141},[14,5788,5789],{},"如果生产者仍在持续写入，要估算 Lag 整体降为 0 的时间，则应扣除生产速度：",[135,5791,5794],{"className":5792,"code":5793,"language":140,"meta":141},[138],"Lag 归零时间\n≈ Lag ÷（消费速度－生产速度）\n",[77,5795,5793],{"__ignoreMap":141},[14,5797,5798],{},"只有消费速度大于生产速度，Lag 才会持续下降。至于当前业务已经延迟多久，应直接查看最老未处理消息的时间戳，而不是只根据 Lag 推算。",[14,5800,5801],{},"消息大小影响的是积压字节量、网络传输和反序列化成本；单条处理复杂度影响的是消费速度。它们都很重要，但不能与 Lag 条数或消息年龄混为同一个指标。",[10,5803,5805],{"id":5804},"二先判断积压发生在哪里","二、先判断积压发生在哪里",[43,5807,5809],{"id":5808},"_1-是所有-partition-积压还是少数-partition-积压","1. 是所有 Partition 积压，还是少数 Partition 积压",[135,5811,5814],{"className":5812,"code":5813,"language":140,"meta":141},[138],"所有 Partition 的 Lag 都增长\n→ 更可能是整体消费能力不足、消费者异常或下游故障\n\n只有少数 Partition 的 Lag 很高\n→ 更可能是 Key 倾斜、热点业务或个别异常消息\n",[77,5815,5813],{"__ignoreMap":141},[14,5817,5818],{},"不能只看 Topic 的总 Lag。总数会掩盖单个热点 Partition，应同时查看每个 Partition 的 Lag、生产速率和消费速率。",[43,5820,5822],{"id":5821},"_2-消费者是否正常工作","2. 消费者是否正常工作",[14,5824,5825],{},"先检查：",[21,5827,5828,5831,5834,5837,5843,5846],{},[24,5829,5830],{},"消费实例是否存活，是否频繁重启。",[24,5832,5833],{},"Consumer Group 中是否存在空闲或反复加入、退出的成员。",[24,5835,5836],{},"是否频繁发生重平衡。",[24,5838,5839,5842],{},[77,5840,5841],{},"poll()"," 是否长时间没有被调用。",[24,5844,5845],{},"Offset 是否持续推进。",[24,5847,5848],{},"消费日志中是否存在反序列化、鉴权、提交 Offset 或业务异常。",[14,5850,5851,5852,5855],{},"如果业务处理时间超过 ",[77,5853,5854],{},"max.poll.interval.ms","，消费者会被认为没有正常推进消费，Partition 可能被重新分配。消费者不断处理、超时、重平衡，又可能造成更严重的积压。",[43,5857,5859],{"id":5858},"_3-单条消息处理是否变慢","3. 单条消息处理是否变慢",[14,5861,5862],{},"Kafka 消费本身往往不是最慢的部分，真正瓶颈通常在业务逻辑：",[21,5864,5865,5868,5871,5874,5877,5880],{},[24,5866,5867],{},"数据库慢 SQL、锁等待或连接池耗尽。",[24,5869,5870],{},"调用外部 RPC 超时。",[24,5872,5873],{},"每条消息单独写库，缺少批处理。",[24,5875,5876],{},"序列化、解压或大对象处理消耗 CPU。",[24,5878,5879],{},"消费逻辑加了粒度过大的锁。",[24,5881,5882],{},"下游 Elasticsearch、Redis 或第三方接口限流。",[14,5884,5885],{},"应把消费耗时拆分为：",[135,5887,5890],{"className":5888,"code":5889,"language":140,"meta":141},[138],"拉取耗时\n+ 反序列化耗时\n+ 业务计算耗时\n+ 数据库 \u002F RPC \u002F 下游写入耗时\n+ Offset 提交耗时\n",[77,5891,5889],{"__ignoreMap":141},[14,5893,5894],{},"只有找到主要耗时，优化才有方向。",[43,5896,5898],{"id":5897},"_4-是否被毒消息阻塞","4. 是否被“毒消息”阻塞",[14,5900,5901],{},"毒消息是指某条数据因为格式错误、业务状态异常或下游约束问题，每次消费都会失败。",[14,5903,5904],{},"如果代码采用无限重试：",[135,5906,5909],{"className":5907,"code":5908,"language":140,"meta":141},[138],"消费失败\n→ 立即重试\n→ 再次失败\n→ 一直占住当前 Partition\n",[77,5910,5908],{"__ignoreMap":141},[14,5912,5913],{},"该 Partition 后续所有消息都可能无法继续处理，表现为单分区 Lag 持续增大。",[43,5915,5917],{"id":5916},"_5-broker-或基础设施是否成为瓶颈","5. Broker 或基础设施是否成为瓶颈",[14,5919,5920],{},"还要检查：",[21,5922,5923,5926,5929,5932,5935],{},[24,5924,5925],{},"Broker 磁盘延迟、网络带宽和 CPU。",[24,5927,5928],{},"Fetch 请求延迟和错误率。",[24,5930,5931],{},"ISR 收缩、离线分区和副本同步异常。",[24,5933,5934],{},"消费者所在机器的 CPU、内存、GC 和网络。",[24,5936,5937],{},"是否发生跨机房访问或网络抖动。",[14,5939,5940],{},"如果 Broker 或网络已经饱和，单纯增加 Consumer 可能只会制造更多请求和竞争。",[10,5942,5944],{"id":5943},"三紧急处理先止住-lag-增长","三、紧急处理：先止住 Lag 增长",[43,5946,5948],{"id":5947},"_1-恢复异常消费者和下游","1. 恢复异常消费者和下游",[14,5950,5951],{},"如果积压源于实例宕机、数据库故障或下游超时，应优先恢复根因：",[135,5953,5956],{"className":5954,"code":5955,"language":140,"meta":141},[138],"恢复消费实例\n→ 恢复数据库 \u002F RPC \u002F 下游\n→ 确认 Offset 开始推进\n→ 再评估是否需要扩容\n",[77,5957,5955],{"__ignoreMap":141},[14,5959,5960],{},"下游仍不可用时强行增加消费者，只会放大连接数、重试和超时流量。",[43,5962,5964],{"id":5963},"_2-对非核心生产流量限流或降级","2. 对非核心生产流量限流或降级",[14,5966,5967],{},"如果生产速度仍在持续上升，而消费端已经满负荷，可以在业务允许时：",[21,5969,5970,5973,5976,5979],{},[24,5971,5972],{},"限制非核心事件的生产速率。",[24,5974,5975],{},"暂停可延迟的定时任务和批量导入。",[24,5977,5978],{},"合并低价值事件。",[24,5980,5981],{},"对非核心功能降级。",[14,5983,5984],{},"这不是最终解决方案，但能降低流入速度，给消费端争取恢复窗口。",[14,5986,5987],{},"这里的“合并低价值事件”，不是随意删除消息，而是对业务允许只保留最终状态或聚合结果的事件进行压缩。例如：",[135,5989,5992],{"className":5990,"code":5991,"language":140,"meta":141},[138],"同一个商品在 1 秒内连续产生 20 次缓存刷新通知\n→ 只保留一次刷新通知\n\n同一设备连续上报多次进度：10%、20%、30%\n→ 如果业务只关心最新进度，可以只发送 30%\n\n大量指标增量事件\n→ 在生产端按时间窗口汇总后，发送一条聚合结果\n",[77,5993,5991],{"__ignoreMap":141},[14,5995,5996],{},"合并前必须先定义清楚业务语义，包括按什么 Key 合并、合并时间窗口多长、保留最新值还是累计值，以及异常时如何补偿。订单状态流转、支付、账户流水、库存扣减、审计日志等每条事件都具有独立业务意义，不能使用这种方式合并。",[43,5998,6000],{"id":5999},"_3-确认消息保留时间是否足够","3. 确认消息保留时间是否足够",[14,6002,6003],{},"积压消息依赖 Kafka 的日志保留策略。如果预计追赶需要数小时或数天，应确认 Topic 的保留时间和磁盘空间是否足够。",[14,6005,6006],{},"如果最老未消费消息超过保留期限，旧日志可能被清理。此时即使消费者恢复，也无法从 Kafka 读取已经删除的数据。",[14,6008,6009],{},"紧急情况下可以评估临时延长保留时间，但必须同步评估 Broker 磁盘容量，避免积压尚未解决又触发磁盘告警。",[10,6011,6013],{"id":6012},"四提升消费能力","四、提升消费能力",[43,6015,6017],{"id":6016},"_1-增加-consumer-实例","1. 增加 Consumer 实例",[14,6019,6020],{},"增加同一 Consumer Group 中的实例，可以让更多 Partition 并行消费。",[14,6022,6023],{},"但有效并行度受 Partition 数量限制：",[135,6025,6028],{"className":6026,"code":6027,"language":140,"meta":141},[138],"8 个 Partition + 4 个 Consumer\n→ 可以继续扩容\n\n8 个 Partition + 8 个 Consumer\n→ 已接近分区级并行上限\n\n8 个 Partition + 12 个 Consumer\n→ 约 4 个 Consumer 无 Partition 可处理\n",[77,6029,6027],{"__ignoreMap":141},[14,6031,6032],{},"因此扩容前要同时查看当前 Partition 数量、消费者数量和分配情况。",[43,6034,6036],{"id":6035},"_2-优化单实例处理吞吐","2. 优化单实例处理吞吐",[14,6038,6039],{},"常见优化方式：",[21,6041,6042,6045,6048,6051,6054,6057,6060],{},[24,6043,6044],{},"把逐条写数据库改为批量写入。",[24,6046,6047],{},"合并相同业务 Key 的重复更新。",[24,6049,6050],{},"优化 SQL、索引和事务范围。",[24,6052,6053],{},"减少消费线程中的同步远程调用。",[24,6055,6056],{},"对可并行步骤使用有界线程池。",[24,6058,6059],{},"复用连接和客户端，避免每条消息重复创建。",[24,6061,6062],{},"降低无必要的日志和对象序列化开销。",[14,6064,106],{},[135,6066,6069],{"className":6067,"code":6068,"language":140,"meta":141},[138],"原方案：\n每条消息 → 一次数据库事务\n\n优化后：\n一批消息 → 分组校验 → 批量写入 → 提交对应 Offset\n",[77,6070,6068],{"__ignoreMap":141},[14,6072,6073,6074,401],{},"批量越大不一定越好。批次过大会增加内存占用、事务时间和失败重试成本，还可能让一次处理时间超过 ",[77,6075,5854],{},[14,6077,6078],{},"这里的“同步远程调用”，是指消费线程发送 RPC 或 HTTP 请求后，必须等待对方返回才能继续处理下一条消息：",[135,6080,6083],{"className":6081,"code":6082,"language":140,"meta":141},[138],"消费一条消息\n→ 调用远程库存服务\n→ 阻塞等待 50 ms\n→ 收到结果\n→ 再处理下一条消息\n",[77,6084,6082],{"__ignoreMap":141},[14,6086,6087],{},"即使本地计算只需要 1 ms，线程的大部分时间也消耗在网络等待上。如果每条消息都串行调用一次远程服务，单线程理论吞吐很容易被远程调用耗时限制。",[14,6089,6090],{},"“减少同步 RPC”可以从以下方向处理：",[21,6092,6093,6096,6099,6102,6105],{},[24,6094,6095],{},"把逐条调用改为批量接口，例如一次查询或提交 100 条。",[24,6097,6098],{},"对可复用且允许短时间不一致的数据使用本地缓存，减少重复查询。",[24,6100,6101],{},"把互不依赖的调用改成受控并发，但必须限制并发数，避免压垮下游。",[24,6103,6104],{},"将通知、埋点等非核心副作用发送到下游 Topic，由独立消费者异步执行。",[24,6106,6107],{},"合理设置超时、熔断和连接池，避免故障调用长期占住消费线程。",[14,6109,6110],{},"如果必须拿到远程结果才能保证当前消息处理正确，就不能为了吞吐简单删除该 RPC。此时更重要的是批量化、限制并发、幂等和下游容量治理。",[43,6112,6114],{"id":6113},"_3-谨慎调整消费参数","3. 谨慎调整消费参数",[14,6116,6117],{},"常见参数包括：",[135,6119,6121],{"className":3590,"code":6120,"language":3592,"meta":141,"style":141},"max.poll.records=500\nmax.poll.interval.ms=300000\nfetch.min.bytes=1\nfetch.max.wait.ms=500\nmax.partition.fetch.bytes=1048576\n",[77,6122,6123,6128,6133,6137,6141],{"__ignoreMap":141},[495,6124,6125],{"class":497,"line":498},[495,6126,6127],{},"max.poll.records=500\n",[495,6129,6130],{"class":497,"line":504},[495,6131,6132],{},"max.poll.interval.ms=300000\n",[495,6134,6135],{"class":497,"line":510},[495,6136,5088],{},[495,6138,6139],{"class":497,"line":661},[495,6140,5093],{},[495,6142,6143],{"class":497,"line":2251},[495,6144,6145],{},"max.partition.fetch.bytes=1048576\n",[14,6147,6148],{},"调整原则：",[21,6150,6151,6161,6168,6174],{},[24,6152,6153,6154,6157,6158,6160],{},"如果一批消息处理不完，应适当减小 ",[77,6155,6156],{},"max.poll.records","，或优化处理速度，而不是只把 ",[77,6159,5854],{}," 调得无限大。",[24,6162,6163,6164,6167],{},"增大 ",[77,6165,6166],{},"fetch.min.bytes"," 可以提高批量拉取效率，但会增加低流量时的等待延迟。",[24,6169,6170,6173],{},[77,6171,6172],{},"max.partition.fetch.bytes"," 过小可能限制大批次拉取，但调大后要评估客户端内存。",[24,6175,6176],{},"参数优化只能降低 Kafka 拉取开销，无法解决数据库或 RPC 本身处理缓慢。",[43,6178,6180],{"id":6179},"_4-异步处理必须有边界","4. 异步处理必须有边界",[14,6182,6183],{},"一种常见设计是由 Poll 线程负责拉取，再把消息交给工作线程池处理：",[135,6185,6188],{"className":6186,"code":6187,"language":140,"meta":141},[138],"Poll 线程\n→ 有界队列\n→ 工作线程池\n→ 处理完成\n→ 按 Partition 推进可安全提交的 Offset\n",[77,6189,6187],{"__ignoreMap":141},[14,6191,6192],{},"为什么必须使用有界队列？假设 Kafka 已经积压 100 万条，Poll 线程仍然高速拉取并塞入无界队列，而工作线程每秒只能处理 100 条，那么积压只是从 Kafka 的磁盘日志转移到了 JVM 堆内存。结果可能是：",[135,6194,6197],{"className":6195,"code":6196,"language":140,"meta":141},[138],"队列持续增长\n→ 堆内存占用上升\n→ Full GC 频繁\n→ 消费进程响应变慢甚至 OOM\n→ 未提交的内存任务随进程退出而丢失并被重新投递\n",[77,6198,6196],{"__ignoreMap":141},[14,6200,6201],{},"Kafka 本身就是持久化队列，没有必要把尚未具备处理能力的消息提前搬进易失的 JVM 内存。",[5164,6203,6205,6206,1955,6209],{"id":6204},"使用高低水位控制-pause-和-resume","使用高、低水位控制 ",[77,6207,6208],{},"pause()",[77,6210,6211],{},"resume()",[14,6213,6214],{},"可以为队列设置容量和两个阈值。例如队列容量为 10000：",[135,6216,6219],{"className":6217,"code":6218,"language":140,"meta":141},[138],"高水位：8000\n队列达到高水位\n→ Poll 线程对相关 Partition 调用 pause()\n→ 后续 poll() 暂停返回这些 Partition 的新消息\n\n低水位：3000\n工作线程处理后，队列下降到低水位\n→ Poll 线程调用 resume()\n→ 后续 poll() 重新拉取这些 Partition\n",[77,6220,6218],{"__ignoreMap":141},[14,6222,6223],{},"使用两个不同阈值是为了避免队列在一个临界值附近反复暂停、恢复。阈值不能照搬固定数字，应根据单条消息大小、堆内存、工作线程数和峰值处理耗时进行容量评估。",[14,6225,6226,6228,6229,6231],{},[77,6227,6208],{}," 的含义是暂停指定 Partition 在后续 ",[77,6230,5841],{}," 中返回新记录，它不会：",[21,6233,6234,6237,6240,6243],{},[24,6235,6236],{},"取消当前 Partition 的分配。",[24,6238,6239],{},"自动提交 Offset。",[24,6241,6242],{},"主动触发 Consumer Group 重平衡。",[24,6244,6245],{},"清除已经拉取并放入本地队列的消息。",[14,6247,6248,6249,6251,6252,6254],{},"暂停期间仍然必须持续调用 ",[77,6250,5841],{},"，让 Consumer 处理心跳、协调和重平衡事件。不能因为所有分区都已暂停，就让 Poll 线程长时间睡眠或等待队列清空，否则仍可能超过 ",[77,6253,5854],{}," 并失去分区。",[5164,6256,6258],{"id":6257},"只能由-consumer-所在线程控制拉取","只能由 Consumer 所在线程控制拉取",[14,6260,6261,6264,6265,220,6267,220,6269,6271],{},[77,6262,6263],{},"KafkaConsumer"," 不是线程安全的。通常由唯一的 Poll 线程持有并调用 ",[77,6266,5841],{},[77,6268,6208],{},[77,6270,6211],{}," 和提交 Offset；工作线程只处理业务，并把“完成结果”写回一个线程安全的完成队列。工作线程不能直接操作同一个 Consumer。",[14,6273,6274],{},"伪代码可以理解为：",[135,6276,6279],{"className":6277,"code":6278,"language":140,"meta":141},[138],"Poll 线程循环：\n    读取工作线程上报的完成结果\n    推进每个 Partition 的连续成功 Offset\n    根据队列水位执行 pause \u002F resume\n    poll() 拉取允许继续消费的 Partition\n    把记录投递到对应的有界工作队列\n\n工作线程：\n    处理业务\n    成功或失败结果写回完成队列\n    不直接调用 KafkaConsumer\n",[77,6280,6278],{"__ignoreMap":141},[5164,6282,6284],{"id":6283},"为什么要按-partition-提交连续成功区间","为什么要按 Partition 提交“连续成功区间”",[14,6286,6287],{},"假设同一 Partition 拉取到：",[135,6289,6292],{"className":6290,"code":6291,"language":140,"meta":141},[138],"Offset 100、101、102\n",[77,6293,6291],{"__ignoreMap":141},[14,6295,6296],{},"并发处理结果是：",[135,6298,6301],{"className":6299,"code":6300,"language":140,"meta":141},[138],"100 成功\n101 仍在处理\n102 成功\n",[77,6302,6300],{"__ignoreMap":141},[14,6304,6305],{},"此时最多只能提交 101，也就是声明 100 已处理完成；不能提交 103。否则进程在 101 完成前崩溃，重启后会从 103 开始，101 就可能被永久跳过。",[14,6307,6308],{},"只有 101 也成功后，100～102 形成连续成功区间，才能提交 103。若业务要求同一 Partition 严格有序，最简单的方式是同一 Partition 串行处理，不要并发执行。",[5164,6310,6311],{"id":6311},"重平衡时还要处理本地未完成任务",[14,6313,6314],{},"发生 Partition 撤销时，应停止向被撤销 Partition 的队列继续投递，并在回调允许的时间内提交已经连续处理成功的 Offset。尚未完成或来不及安全提交的任务，应允许新 Consumer 从旧 Offset 重新消费，因此业务处理必须具备幂等性。",[14,6316,6317],{},"需要共同遵守的约束包括：",[21,6319,6320,6325,6328,6335,6340,6343,6346],{},[24,6321,6322,6324],{},[77,6323,6263],{}," 本身不是线程安全的，不能让多个工作线程同时操作同一个 Consumer。",[24,6326,6327],{},"队列必须有界，不能把 Kafka 的积压无上限转移到 JVM 内存。",[24,6329,6330,5142,6332,6334],{},[77,6331,6208],{},[77,6333,6211],{}," 应由 Poll 线程执行，工作线程通过控制信号提出请求。",[24,6336,6337,6338,401],{},"即使 Partition 已暂停，Poll 线程也要持续执行 ",[77,6339,5841],{},[24,6341,6342],{},"Offset 必须在消息真正处理成功后提交。",[24,6344,6345],{},"同一 Partition 如果要求严格顺序，不能让后面的消息先于前面的消息产生业务效果。",[24,6347,6348],{},"并发完成后只能提交“连续成功区间”的下一个 Offset，不能跨过尚未完成或失败的消息。",[14,6350,6351],{},"异步化能提高吞吐，但会显著增加 Offset、顺序和失败处理的复杂度。",[10,6353,6355],{"id":6354},"五partition-不够时怎么办","五、Partition 不够时怎么办",[43,6357,6359],{"id":6358},"_1-增加-partition-主要提升未来并行度","1. 增加 Partition 主要提升未来并行度",[14,6361,6362],{},"如果 Consumer 数量已经等于 Partition 数量，且单分区处理能力也接近上限，可以评估增加 Partition。",[14,6364,6365],{},"但要明确：",[47,6367,6368],{},[14,6369,6370],{},[213,6371,6372],{},"给 Topic 新增 Partition，不会把旧 Partition 中已经积压的消息自动重新分布到新 Partition。",[14,6374,6375],{},"新 Partition 主要承接之后产生的新消息。旧积压仍然留在原来的 Partition 中，仍需由对应消费者追赶。",[43,6377,6379],{"id":6378},"_2-增加-partition-可能改变-key-路由","2. 增加 Partition 可能改变 Key 路由",[14,6381,6382],{},"默认的 Key 哈希结果通常与 Partition 数量有关。增加 Partition 后，同一个 Key 的新消息可能进入不同于历史消息的 Partition，从而影响跨扩容时点的顺序假设。",[14,6384,6385],{},"例如为了便于理解，假设某个分区规则可以简化为：",[135,6387,6390],{"className":6388,"code":6389,"language":140,"meta":141},[138],"目标分区 = hash(key) % Partition 数量\n",[77,6391,6389],{"__ignoreMap":141},[14,6393,6394],{},"同一个 Key 的哈希值为 13：",[135,6396,6399],{"className":6397,"code":6398,"language":140,"meta":141},[138],"原来 4 个 Partition：13 % 4 = 1，写入 P1\n扩容到 8 个 Partition：13 % 8 = 5，新消息写入 P5\n",[77,6400,6398],{"__ignoreMap":141},[14,6402,6403],{},"扩容前已经写入 P1 的旧消息不会被搬到 P5。P1 和 P5 又可能由不同 Consumer 并行处理，因此 P5 中的新消息可能先于 P1 中的旧消息完成。这个风险不只是“扩容瞬间”存在，而是会持续到旧分区中的相关历史消息全部处理完毕。",[14,6405,6406],{},"所谓“消费端能否接受跨分区处理”，实际是在问：",[47,6408,6409],{},[14,6410,6411],{},"同一个业务 Key 的旧消息和新消息同时位于不同 Partition，并可能并行、乱序完成时，业务是否仍然正确？",[14,6413,6414],{},"如果事件是独立日志或幂等的最终状态覆盖，业务可能可以接受；如果事件依赖严格顺序，例如“创建订单 → 支付成功 → 关闭订单”，就不能直接接受。",[14,6416,6417],{},"如果业务依赖同一 Key 的严格顺序，常见选择有：",[5164,6419,6421],{"id":6420},"方案一排空旧消息后再扩分区","方案一：排空旧消息后再扩分区",[135,6423,6426],{"className":6424,"code":6425,"language":140,"meta":141},[138],"限制或暂停生产\n→ 等待旧 Topic 的相关消息处理完成\n→ 增加 Partition\n→ 恢复生产\n",[77,6427,6425],{"__ignoreMap":141},[14,6429,6430],{},"这种方式逻辑最清楚，但需要维护窗口，而且在高流量系统中可能很难完全停写。",[5164,6432,6434],{"id":6433},"方案二新建-topic-并进行受控切换","方案二：新建 Topic 并进行受控切换",[14,6436,6437,6438,3079],{},"例如创建具有目标分区数和新路由规则的 ",[77,6439,6440],{},"order-events-v2",[135,6442,6445],{"className":6443,"code":6444,"language":140,"meta":141},[138],"1. 创建并验证新 Topic\n2. 定义明确的切换时间、版本号或业务水位\n3. 新消息从切换点开始写入 v2\n4. v1 消费者继续处理切换点以前的旧消息\n5. 确认某个 Key 或全部旧消息越过安全水位后，再允许 v2 对应消息生效\n6. 完成核对后停止 v1，并保留回滚方案\n",[77,6446,6444],{"__ignoreMap":141},[14,6448,6449],{},"新建 Topic 的价值是把新旧路由、配置和消费组隔离开，便于验证与回滚；代价是必须设计双 Topic 切换、重复消息、遗漏检查和新旧 Offset 衔接。它不是 Kafka 自动迁移，而是业务自己完成的版本化切换。",[14,6451,6452],{},"因此扩分区前应明确回答：",[21,6454,6455,6458,6461,6464],{},[24,6456,6457],{},"分区器如何计算目标 Partition。",[24,6459,6460],{},"旧消息是否已经处理完成。",[24,6462,6463],{},"同一 Key 跨新旧 Partition 并行处理是否会破坏业务顺序。",[24,6465,6466],{},"是选择维护窗口排空旧消息，还是新建 Topic 做受控切换。",[43,6468,6470],{"id":6469},"_3-极端情况下建立临时加速链路","3. 极端情况下建立临时加速链路",[14,6472,6473],{},"当原 Topic 分区数严重不足、积压量巨大时，可以设计经过评审的临时方案：",[135,6475,6478],{"className":6476,"code":6477,"language":140,"meta":141},[138],"原 Topic\n→ 临时搬运程序\n→ 更高分区数的中转 Topic\n→ 临时扩容后的消费组\n→ 业务处理\n",[77,6479,6477],{"__ignoreMap":141},[14,6481,6482],{},"这会引入顺序、重复、Offset 衔接和回切问题，只适合经过完整设计和演练的场景，不能在生产事故中临时拍脑袋执行。",[10,6484,6486],{"id":6485},"六热点-partition-如何处理","六、热点 Partition 如何处理",[14,6488,6489],{},"如果只有少数 Partition 积压，需要检查消息 Key 分布。",[14,6491,6492],{},"例如所有消息都使用同一个固定 Key：",[135,6494,6497],{"className":6495,"code":6496,"language":140,"meta":141},[138],"key = \"default\"\n→ 所有消息进入同一 Partition\n→ 其他 Partition 空闲\n→ 单分区成为瓶颈\n",[77,6498,6496],{"__ignoreMap":141},[14,6500,6501],{},"解决方向包括：",[21,6503,6504,6507,6510,6516,6519],{},[24,6505,6506],{},"选择分布更均匀的业务 Key。",[24,6508,6509],{},"对非顺序场景取消固定 Key。",[24,6511,6512,6513,401],{},"对热点业务 Key 做可控分片，例如 ",[77,6514,6515],{},"userId + shardNo",[24,6517,6518],{},"单独拆分热点租户或热点业务到独立 Topic。",[24,6520,6521],{},"优化该 Partition 对应消费者的单条处理耗时。",[14,6523,6524],{},"Key 拆分会改变顺序语义。原来同一 Key 的全局顺序，拆分后通常只能保证每个子 Key 内部有序。",[10,6526,6528],{"id":6527},"七毒消息和重试如何处理","七、毒消息和重试如何处理",[14,6530,6531],{},"不要让一条永远失败的消息无限阻塞 Partition。常见处理策略是：",[135,6533,6536],{"className":6534,"code":6535,"language":140,"meta":141},[138],"第一次失败\n→ 记录错误并有限重试\n→ 仍然失败\n→ 进入重试 Topic \u002F 死信 Topic\n→ 主消费链路继续\n→ 告警和人工补偿\n",[77,6537,6535],{"__ignoreMap":141},[14,6539,6540],{},"需要保留：",[21,6542,6543,6546,6549,6552],{},[24,6544,6545],{},"原始 Topic、Partition 和 Offset。",[24,6547,6548],{},"消息 Key、事件 ID 和原始内容。",[24,6550,6551],{},"异常原因和重试次数。",[24,6553,6554],{},"首次失败和最后失败时间。",[14,6556,6557],{},"如果业务要求同一 Key 严格顺序，就不能简单跳过失败消息继续处理后续消息。此时应暂停对应 Partition，优先修复或补偿该消息；这是吞吐与顺序之间的业务取舍。",[10,6559,6561],{"id":6560},"八如何估算多久能消化完积压","八、如何估算多久能消化完积压",[14,6563,6564],{},"设：",[135,6566,6569],{"className":6567,"code":6568,"language":140,"meta":141},[138],"B = 当前积压消息数\nP = 当前生产速度（条\u002F秒）\nC = 扩容后的稳定消费速度（条\u002F秒）\n",[77,6570,6568],{"__ignoreMap":141},[14,6572,6573],{},"只有：",[135,6575,6578],{"className":6576,"code":6577,"language":140,"meta":141},[138],"C > P\n",[77,6579,6577],{"__ignoreMap":141},[14,6581,6582],{},"积压才会下降。理论恢复时间约为：",[135,6584,6587],{"className":6585,"code":6586,"language":140,"meta":141},[138],"恢复时间 T = B \u002F (C - P)\n",[77,6588,6586],{"__ignoreMap":141},[14,6590,106],{},[135,6592,6595],{"className":6593,"code":6594,"language":140,"meta":141},[138],"当前积压 B = 360 万条\n生产速度 P = 2000 条\u002F秒\n消费速度 C = 5000 条\u002F秒\n\n净消化速度 = 5000 - 2000 = 3000 条\u002F秒\n理论恢复时间 = 3600000 \u002F 3000 = 1200 秒\n≈ 20 分钟\n",[77,6596,6594],{"__ignoreMap":141},[14,6598,6599],{},"实际还要为重平衡、失败重试、下游波动和流量峰值预留余量。",[14,6601,797,6602,6605],{},[77,6603,6604],{},"C \u003C= P","，无论运行多久都追不上，必须降低生产速率或提高有效消费能力。",[10,6607,6609],{"id":6608},"九offset-跳到最新为什么不是常规方案","九、Offset 跳到最新为什么不是常规方案",[14,6611,6612],{},"把消费组 Offset 重置到最新位置，确实可以让 Lag 迅速变成 0，但其本质是：",[47,6614,6615],{},[14,6616,6617],{},[213,6618,6619],{},"主动放弃尚未消费的历史消息。",[14,6621,6622],{},"只有同时满足以下条件才可以考虑：",[21,6624,6625,6628,6631,6634,6637],{},[24,6626,6627],{},"业务明确确认旧消息可以丢弃。",[24,6629,6630],{},"已评估对账、状态和下游影响。",[24,6632,6633],{},"已保留必要的审计或补偿数据。",[24,6635,6636],{},"操作前进行了 Dry Run，并确认 Topic、Consumer Group 和目标 Offset。",[24,6638,6639],{},"操作期间停止相关消费者，避免 Offset 并发变化。",[14,6641,6642],{},"资金、订单、库存等关键链路不能把重置 Offset 当作普通清积压手段。",[43,6644,6646],{"id":6645},"什么是-dry-run","什么是 Dry Run",[14,6648,6649],{},"Dry Run 就是“只预演，不执行”。它先计算并展示每个 Partition 将从哪个 Offset 调整到哪个 Offset，供操作人员核对 Topic、消费组、分区范围和目标位置，但不真正修改已提交 Offset。",[14,6651,6652],{},"例如将消费组预演重置到最新位置：",[135,6654,6658],{"className":6655,"code":6656,"language":6657,"meta":141,"style":141},"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",[77,6659,6660,6670,6681,6691,6701,6708],{"__ignoreMap":141},[495,6661,6662,6666],{"class":497,"line":498},[495,6663,6665],{"class":6664},"sScJk","bin\u002Fkafka-consumer-groups.sh",[495,6667,6669],{"class":6668},"sj4cs"," \\\n",[495,6671,6672,6675,6679],{"class":497,"line":504},[495,6673,6674],{"class":6668},"  --bootstrap-server",[495,6676,6678],{"class":6677},"sZZnC"," kafka.example.com:9092",[495,6680,6669],{"class":6668},[495,6682,6683,6686,6689],{"class":497,"line":510},[495,6684,6685],{"class":6668},"  --group",[495,6687,6688],{"class":6677}," order-consumer-group",[495,6690,6669],{"class":6668},[495,6692,6693,6696,6699],{"class":497,"line":661},[495,6694,6695],{"class":6668},"  --topic",[495,6697,6698],{"class":6677}," order-events",[495,6700,6669],{"class":6668},[495,6702,6703,6706],{"class":497,"line":2251},[495,6704,6705],{"class":6668},"  --reset-offsets",[495,6707,6669],{"class":6668},[495,6709,6710],{"class":497,"line":2289},[495,6711,6712],{"class":6668},"  --to-latest\n",[14,6714,6715,6716,6719],{},"Kafka 4.1 的消费组工具默认只展示重置计划；确认结果无误后，显式增加 ",[77,6717,6718],{},"--execute"," 才会真正执行：",[135,6721,6723],{"className":6655,"code":6722,"language":6657,"meta":141,"style":141},"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",[77,6724,6725,6731,6739,6747,6755,6761,6768],{"__ignoreMap":141},[495,6726,6727,6729],{"class":497,"line":498},[495,6728,6665],{"class":6664},[495,6730,6669],{"class":6668},[495,6732,6733,6735,6737],{"class":497,"line":504},[495,6734,6674],{"class":6668},[495,6736,6678],{"class":6677},[495,6738,6669],{"class":6668},[495,6740,6741,6743,6745],{"class":497,"line":510},[495,6742,6685],{"class":6668},[495,6744,6688],{"class":6677},[495,6746,6669],{"class":6668},[495,6748,6749,6751,6753],{"class":497,"line":661},[495,6750,6695],{"class":6668},[495,6752,6698],{"class":6677},[495,6754,6669],{"class":6668},[495,6756,6757,6759],{"class":497,"line":2251},[495,6758,6705],{"class":6668},[495,6760,6669],{"class":6668},[495,6762,6763,6766],{"class":497,"line":2289},[495,6764,6765],{"class":6668},"  --to-latest",[495,6767,6669],{"class":6668},[495,6769,6770],{"class":497,"line":2295},[495,6771,6772],{"class":6668},"  --execute\n",[14,6774,6775],{},"不同 Kafka 版本的工具参数可能略有差异，生产操作前应以实际部署版本的命令帮助和官方文档为准。",[43,6777,6778],{"id":6778},"为什么操作期间要停止消费者",[14,6780,6781,6782,6784],{},"活动消费者会继续执行 ",[77,6783,5841],{},"、处理消息并提交 Offset。如果管理人员同时重置 Offset，就可能发生竞态：",[135,6786,6789],{"className":6787,"code":6788,"language":140,"meta":141},[138],"管理员把 Offset 从 1000 重置到 5000\n→ 仍在运行的消费者随后提交自己内存中的旧进度 1200\n→ 重置结果被旧提交覆盖\n",[77,6790,6788],{"__ignoreMap":141},[14,6792,6793],{},"反过来也可能是消费者刚处理到 1300，管理员基于稍早看到的状态执行重置，导致已处理消息重复消费或尚未处理消息被跳过。活动成员还可能触发重平衡，使操作时看到的分区归属和 Offset 继续变化。",[14,6795,6796],{},"因此安全流程应是：",[135,6798,6801],{"className":6799,"code":6800,"language":140,"meta":141},[138],"停止该消费组的所有消费者\n→ 确认消费组已无活动成员\n→ 记录操作前 Offset\n→ 执行 Dry Run 并双人核对\n→ 使用 --execute 修改 Offset\n→ 再次查询并确认结果\n→ 启动消费者并观察消费位置\n",[77,6802,6800],{"__ignoreMap":141},[14,6804,6805],{},"这不仅是避免并发覆盖。Kafka 官方的消费组 Offset 重置说明也明确要求先确保消费实例处于非活动状态。",[10,6807,6809],{"id":6808},"十恢复后要做什么","十、恢复后要做什么",[14,6811,6812],{},"积压恢复不代表问题结束，还应完成：",[705,6814,6815,6818,6821,6824,6827,6830,6833],{},[24,6816,6817],{},"复盘根因和时间线。",[24,6819,6820],{},"补充总 Lag、最大分区 Lag、时间 Lag 和增长率告警。",[24,6822,6823],{},"建立生产速度、消费速度和预计恢复时间看板。",[24,6825,6826],{},"对热点 Partition、毒消息和下游耗时建立专项监控。",[24,6828,6829],{},"按峰值流量进行容量压测，保留消费冗余。",[24,6831,6832],{},"演练消费者扩容、下游故障、重平衡和死信补偿。",[24,6834,6835],{},"核对积压期间是否发生消息过期、重复处理或业务遗漏。",[10,6837,6839],{"id":6838},"十一常见误区","十一、常见误区",[43,6841,6843],{"id":6842},"误区一积压就直接增加-consumer","误区一：积压就直接增加 Consumer",[14,6845,6846],{},"如果 Partition 数量不足、多数 Consumer 已空闲，或者瓶颈实际在数据库，增加 Consumer 不会解决问题，反而可能加剧下游压力。",[43,6848,6850,6851,6853],{"id":6849},"误区二把-maxpollrecords-调大就能提升吞吐","误区二：把 ",[77,6852,6156],{}," 调大就能提升吞吐",[14,6855,6856],{},"拉取更多消息不等于处理更快。批次过大还可能导致处理超时、重平衡和更高的失败重试成本。",[43,6858,6860],{"id":6859},"误区三增加-partition-会重新分配旧积压","误区三：增加 Partition 会重新分配旧积压",[14,6862,6863],{},"新增 Partition 不会迁移旧 Partition 中已有的消息，只能为后续消息提供更多并行空间。",[43,6865,6867],{"id":6866},"误区四lag-等于业务延迟","误区四：Lag 等于业务延迟",[14,6869,6870,6871,6874],{},"Lag 是 Offset 数量差，只表示还落后多少条记录，不等于时间延迟。相同 Lag 下，消费者处理速度越快，处理完现有积压所需的时间越短；如果生产仍在继续，则应使用 ",[77,6872,6873],{},"Lag ÷（消费速度－生产速度）"," 估算 Lag 归零时间。",[14,6876,6877],{},"当前业务已经延迟多久，应通过最老未处理消息的时间戳判断。消息大小影响积压字节量、网络与反序列化成本，单条处理耗时影响消费速度。因此排查时应把 Lag 条数、最老消息年龄、积压字节量和实际处理吞吐分开观察，不能混成一个指标。",[43,6879,6881],{"id":6880},"误区五把-offset-跳到最新就是清理积压","误区五：把 Offset 跳到最新就是清理积压",[14,6883,6884],{},"这不是“处理了积压”，而是“丢弃了积压”。必须经过明确的业务授权和风险评估。",[10,6886,1007],{"id":1007},[21,6888,6889,6894,6899,6906],{},[24,6890,6891],{},[58,6892,4544],{"href":4542,"rel":6893},[1016],[24,6895,6896],{},[58,6897,4558],{"href":4556,"rel":6898},[1016],[24,6900,6901],{},[58,6902,6905],{"href":6903,"rel":6904},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Foperations\u002Fconsumer-rebalance-protocol\u002F",[1016],"Apache Kafka Consumer Rebalance Protocol",[24,6907,6908],{},[58,6909,6912],{"href":6910,"rel":6911},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Foperations\u002Fbasic-kafka-operations\u002F",[1016],"Apache Kafka Basic Operations",[1047,6914,6915],{},"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 .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}",{"title":141,"searchDepth":504,"depth":504,"links":6917},[6918,6919,6920,6921,6928,6933,6939,6944,6945,6946,6947,6951,6952,6960],{"id":12,"depth":504,"text":12},{"id":54,"depth":504,"text":54},{"id":5677,"depth":504,"text":5678},{"id":5804,"depth":504,"text":5805,"children":6922},[6923,6924,6925,6926,6927],{"id":5808,"depth":510,"text":5809},{"id":5821,"depth":510,"text":5822},{"id":5858,"depth":510,"text":5859},{"id":5897,"depth":510,"text":5898},{"id":5916,"depth":510,"text":5917},{"id":5943,"depth":504,"text":5944,"children":6929},[6930,6931,6932],{"id":5947,"depth":510,"text":5948},{"id":5963,"depth":510,"text":5964},{"id":5999,"depth":510,"text":6000},{"id":6012,"depth":504,"text":6013,"children":6934},[6935,6936,6937,6938],{"id":6016,"depth":510,"text":6017},{"id":6035,"depth":510,"text":6036},{"id":6113,"depth":510,"text":6114},{"id":6179,"depth":510,"text":6180},{"id":6354,"depth":504,"text":6355,"children":6940},[6941,6942,6943],{"id":6358,"depth":510,"text":6359},{"id":6378,"depth":510,"text":6379},{"id":6469,"depth":510,"text":6470},{"id":6485,"depth":504,"text":6486},{"id":6527,"depth":504,"text":6528},{"id":6560,"depth":504,"text":6561},{"id":6608,"depth":504,"text":6609,"children":6948},[6949,6950],{"id":6645,"depth":510,"text":6646},{"id":6778,"depth":510,"text":6778},{"id":6808,"depth":504,"text":6809},{"id":6838,"depth":504,"text":6839,"children":6953},[6954,6955,6957,6958,6959],{"id":6842,"depth":510,"text":6843},{"id":6849,"depth":510,"text":6956},"误区二：把 max.poll.records 调大就能提升吞吐",{"id":6859,"depth":510,"text":6860},{"id":6866,"depth":510,"text":6867},{"id":6880,"depth":510,"text":6881},{"id":1007,"depth":504,"text":1007},"wiki:java:kafka-message-backlog-handling","从 Consumer Lag 识别、瓶颈定位、限流止血、消费者扩容、批量处理、热点分区和毒消息治理等方面，系统讲解 Kafka 消息积压处理方案。",{},74,"\u002Fjava\u002Fkafka-message-backlog-handling",{"title":5630,"description":6962},"java\u002Fkafka-message-backlog-handling","j3-dVDOmz1OVfrV7vyfXHjDOrGBfFrh6o3O0nK5yIG4",{"id":6970,"title":6971,"body":6972,"commentId":8383,"description":8384,"difficulty":1082,"draft":1083,"extension":1084,"meta":8385,"navigation":1086,"order":8386,"path":8387,"section":1089,"seo":8388,"stem":8389,"updated":4612,"__hash__":8390},"java\u002Fjava\u002Fmysql-transaction-isolation-and-phantom-read.md","MySQL 的隔离级别都有哪些？如何避免幻读？",{"type":7,"value":6973,"toc":8335},[6974,6976,6979,7016,7019,7033,7036,7038,7043,7045,7049,7052,7057,7060,7063,7144,7146,7151,7155,7159,7162,7168,7174,7178,7181,7187,7190,7195,7199,7202,7208,7211,7214,7219,7222,7230,7234,7238,7241,7252,7255,7259,7262,7265,7271,7274,7280,7288,7292,7295,7301,7307,7310,7313,7317,7320,7326,7335,7338,7341,7345,7348,7352,7358,7367,7370,7381,7384,7388,7391,7415,7418,7421,7425,7428,7452,7459,7483,7486,7499,7502,7505,7510,7513,7517,7520,7547,7554,7563,7566,7569,7582,7589,7592,7597,7601,7605,7608,7610,7627,7633,7637,7640,7643,7649,7652,7658,7661,7665,7668,7674,7677,7683,7686,7692,7712,7715,7720,7723,7741,7754,7760,7763,7769,7772,7775,7799,7806,7809,7827,7831,7834,7837,7854,7857,7860,7863,7868,7870,7875,7878,7895,7899,7902,7922,7925,7928,7931,7945,7948,7963,7966,7970,7976,7979,7985,7988,8000,8003,8006,8010,8013,8021,8024,8032,8035,8049,8053,8057,8059,8073,8077,8079,8093,8097,8099,8113,8116,8136,8140,8143,8152,8155,8164,8167,8176,8179,8188,8191,8199,8202,8217,8228,8231,8239,8242,8244,8248,8251,8255,8258,8266,8270,8273,8277,8280,8284,8287,8291,8294,8296,8333],[10,6975,12],{"id":12},[14,6977,6978],{},"MySQL InnoDB 支持四种事务隔离级别：读未提交、读已提交、可重复读和串行化。",[21,6980,6981,6987,6993,7010],{},[24,6982,6983,6986],{},[213,6984,6985],{},"读未提交（READ UNCOMMITTED）","：可能发生脏读、不可重复读和幻读。",[24,6988,6989,6992],{},[213,6990,6991],{},"读已提交（READ COMMITTED）","：只能读取已经提交的数据，但每次普通查询都会创建新的 Read View，因此可能发生不可重复读和幻读。",[24,6994,6995,6998,6999,220,7002,220,7005,1955,7007,7009],{},[213,6996,6997],{},"可重复读（REPEATABLE READ）","：MySQL InnoDB 的默认隔离级别。同一事务中的普通快照读复用第一次查询建立的 Read View，避免不可重复读，并通过一致性快照避免看到后来插入的幻影行；对于 ",[77,7000,7001],{},"SELECT ... FOR UPDATE",[77,7003,7004],{},"SELECT ... FOR SHARE",[77,7006,976],{},[77,7008,979],{}," 等当前读，则通过 Next-Key Lock 锁住索引记录及其前面的间隙，阻止其他事务在查询范围内插入新记录。",[24,7011,7012,7015],{},[213,7013,7014],{},"串行化（SERIALIZABLE）","：隔离性最强，事务近似串行执行，可以避免脏读、不可重复读和幻读，但并发能力最低。",[14,7017,7018],{},"因此，回答“InnoDB 如何避免幻读”时，需要区分两类读取：",[705,7020,7021,7028],{},[24,7022,7023,7024,7027],{},"普通 ",[77,7025,7026],{},"SELECT"," 属于快照读，REPEATABLE READ 通过 MVCC 和同一个 Read View，让事务始终读取同一份一致性快照。",[24,7029,7030,7032],{},[77,7031,7001],{}," 等属于当前读，REPEATABLE READ 通过 Gap Lock 和 Next-Key Lock 阻止其他事务向锁定范围插入新记录。",[14,7034,7035],{},"如果业务逻辑是“先查询是否存在，再决定是否插入或更新”，仅依赖快照读通常不够，应在 REPEATABLE READ 下使用合适索引和锁定读，并通过唯一索引等数据库约束作为最终兜底。",[43,7037,45],{"id":45},[47,7039,7040],{},[14,7041,7042],{},"InnoDB 默认使用 REPEATABLE READ：普通查询靠 MVCC 的一致性快照避免看见幻影行，当前读靠 Next-Key Lock 锁住记录和间隙，阻止幻影行被插入。",[10,7044,54],{"id":54},[10,7046,7048],{"id":7047},"一事务隔离级别解决什么问题","一、事务隔离级别解决什么问题",[14,7050,7051],{},"多个事务并发执行时，一个事务的写入可能影响另一个事务的读取。隔离级别用于规定：",[47,7053,7054],{},[14,7055,7056],{},"一个事务能够看到其他事务的哪些修改，以及什么时候能够看到这些修改。",[14,7058,7059],{},"隔离级别越高，事务之间的影响越小，但加锁和等待通常越多，并发能力也可能越低。",[14,7061,7062],{},"MySQL InnoDB 支持 SQL 标准定义的四种隔离级别：",[230,7064,7065,7085],{},[233,7066,7067],{},[236,7068,7069,7072,7076,7079,7082],{},[239,7070,7071],{},"隔离级别",[239,7073,7075],{"align":7074},"right","脏读",[239,7077,7078],{"align":7074},"不可重复读",[239,7080,7081],{"align":7074},"幻读",[239,7083,7084],{"align":7074},"并发能力",[249,7086,7087,7102,7116,7130],{},[236,7088,7089,7092,7095,7097,7099],{},[254,7090,7091],{},"READ UNCOMMITTED",[254,7093,7094],{"align":7074},"可能",[254,7096,7094],{"align":7074},[254,7098,7094],{"align":7074},[254,7100,7101],{"align":7074},"最高",[236,7103,7104,7107,7110,7112,7114],{},[254,7105,7106],{},"READ COMMITTED",[254,7108,7109],{"align":7074},"避免",[254,7111,7094],{"align":7074},[254,7113,7094],{"align":7074},[254,7115,2755],{"align":7074},[236,7117,7118,7121,7123,7125,7128],{},[254,7119,7120],{},"REPEATABLE READ",[254,7122,7109],{"align":7074},[254,7124,7109],{"align":7074},[254,7126,7127],{"align":7074},"InnoDB 通过 MVCC 和 Next-Key Lock 处理",[254,7129,2755],{"align":7074},[236,7131,7132,7135,7137,7139,7141],{},[254,7133,7134],{},"SERIALIZABLE",[254,7136,7109],{"align":7074},[254,7138,7109],{"align":7074},[254,7140,7109],{"align":7074},[254,7142,7143],{"align":7074},"最低",[14,7145,3649],{},[47,7147,7148],{},[14,7149,7150],{},"“REPEATABLE READ 可能发生幻读”是 SQL 标准层面的常见结论；MySQL InnoDB 在该级别上又实现了 MVCC 和 Next-Key Lock，对快照读和当前读分别处理幻读问题。",[10,7152,7154],{"id":7153},"二三种并发读取问题","二、三种并发读取问题",[43,7156,7158],{"id":7157},"_1-脏读","1. 脏读",[14,7160,7161],{},"事务 A 读取了事务 B 尚未提交的数据。如果事务 B 最后回滚，事务 A 读到的就是从未真正生效的数据。",[135,7163,7166],{"className":7164,"code":7165,"language":140,"meta":141},[138],"事务 A                         事务 B\n                              修改余额 100 → 0\n读取余额，得到 0\n                              ROLLBACK\n",[77,7167,7165],{"__ignoreMap":141},[14,7169,7170,7171,7173],{},"这里的 ",[77,7172,1345],{}," 就是脏数据。",[43,7175,7177],{"id":7176},"_2-不可重复读","2. 不可重复读",[14,7179,7180],{},"同一事务两次读取同一行，得到的字段值不同。",[135,7182,7185],{"className":7183,"code":7184,"language":140,"meta":141},[138],"事务 A                         事务 B\n读取 id=1，余额为 100\n                              修改余额为 80\n                              COMMIT\n再次读取 id=1，余额为 80\n",[77,7186,7184],{"__ignoreMap":141},[14,7188,7189],{},"不可重复读关注的是：",[47,7191,7192],{},[14,7193,7194],{},"同一行记录的内容发生了变化。",[43,7196,7198],{"id":7197},"_3-幻读","3. 幻读",[14,7200,7201],{},"同一事务使用相同查询条件执行两次查询，第二次返回的结果集合中出现了第一次不存在的记录，或者原有记录消失。",[135,7203,7206],{"className":7204,"code":7205,"language":140,"meta":141},[138],"事务 A                         事务 B\n查询 amount > 100，返回 3 行\n                              插入一条 amount=150 的记录\n                              COMMIT\n再次查询 amount > 100，返回 4 行\n",[77,7207,7205],{"__ignoreMap":141},[14,7209,7210],{},"新增的第 4 行就是幻影行。",[14,7212,7213],{},"幻读关注的是：",[47,7215,7216],{},[14,7217,7218],{},"满足查询条件的记录集合发生了变化。",[14,7220,7221],{},"所以，不可重复读和幻读的区别可以记为：",[21,7223,7224,7227],{},[24,7225,7226],{},"不可重复读：同一行的值变了。",[24,7228,7229],{},"幻读：符合条件的行数或记录集合变了。",[10,7231,7233],{"id":7232},"三四种隔离级别","三、四种隔离级别",[43,7235,7237],{"id":7236},"_1-read-uncommitted读未提交","1. READ UNCOMMITTED：读未提交",[14,7239,7240],{},"事务可以读取其他事务尚未提交的修改，因此可能发生：",[21,7242,7243,7246,7249],{},[24,7244,7245],{},"脏读；",[24,7247,7248],{},"不可重复读；",[24,7250,7251],{},"幻读。",[14,7253,7254],{},"它的隔离性最低，实际业务系统很少使用。",[43,7256,7258],{"id":7257},"_2-read-committed读已提交","2. READ COMMITTED：读已提交",[14,7260,7261],{},"事务只能读取已经提交的数据，因此不会发生脏读。",[14,7263,7264],{},"但在 InnoDB 中，每一次普通一致性读都会创建一个新的 Read View：",[135,7266,7269],{"className":7267,"code":7268,"language":140,"meta":141},[138],"第一次 SELECT → Read View 1\n第二次 SELECT → Read View 2\n第三次 SELECT → Read View 3\n",[77,7270,7268],{"__ignoreMap":141},[14,7272,7273],{},"如果其他事务在两次查询之间提交了修改，后一次查询就可能看到新数据，因此可能发生：",[21,7275,7276,7278],{},[24,7277,7248],{},[24,7279,7251],{},[14,7281,7282,7283,1955,7285,7287],{},"另外，在 READ COMMITTED 下，锁定读、",[77,7284,976],{},[77,7286,979],{}," 通常只锁索引记录，不锁记录前面的间隙。除了外键约束检查和重复键检查等情况外，Gap Lock 被禁用，因此其他事务仍可能向间隙插入新记录。",[43,7289,7291],{"id":7290},"_3-repeatable-read可重复读","3. REPEATABLE READ：可重复读",[14,7293,7294],{},"这是 InnoDB 的默认隔离级别。",[14,7296,7297,7298,7300],{},"对于普通 ",[77,7299,7026],{},"，事务中的第一次一致性读会建立 Read View，之后的普通一致性读继续使用该快照：",[135,7302,7305],{"className":7303,"code":7304,"language":140,"meta":141},[138],"第一次普通 SELECT → 创建 Read View\n第二次普通 SELECT → 复用同一个 Read View\n第三次普通 SELECT → 复用同一个 Read View\n",[77,7306,7304],{"__ignoreMap":141},[14,7308,7309],{},"因此，其他事务之后提交的更新、删除或插入不会出现在当前事务的旧快照中。",[14,7311,7312],{},"对于锁定读和数据修改语句，InnoDB 会读取最新可用数据，并根据查询使用的索引范围加锁。范围条件通常会使用 Gap Lock 或 Next-Key Lock，阻止其他事务向被锁定的范围插入新记录。",[43,7314,7316],{"id":7315},"_4-serializable串行化","4. SERIALIZABLE：串行化",[14,7318,7319],{},"SERIALIZABLE 是隔离性最强的级别。",[14,7321,7322,7323,7325],{},"在关闭自动提交的情况下，InnoDB 会把普通 ",[77,7324,7026],{}," 隐式转换为共享锁定读，效果类似：",[135,7327,7329],{"className":489,"code":7328,"language":491,"meta":141,"style":141},"SELECT ... FOR SHARE;\n",[77,7330,7331],{"__ignoreMap":141},[495,7332,7333],{"class":497,"line":498},[495,7334,7328],{},[14,7336,7337],{},"这样事务之间会产生更多锁等待，能够避免脏读、不可重复读和幻读，但吞吐量通常明显下降。",[14,7339,7340],{},"它适合一致性要求极高、并发量较低，或者确实需要事务串行执行的场景。",[10,7342,7344],{"id":7343},"四快照读与当前读","四、快照读与当前读",[14,7346,7347],{},"理解 InnoDB 如何处理幻读，必须先区分快照读和当前读。",[43,7349,7351],{"id":7350},"_1-快照读","1. 快照读",[14,7353,7354,7355,7357],{},"在 READ COMMITTED 和 REPEATABLE READ 下，普通 ",[77,7356,7026],{}," 通常属于快照读：",[135,7359,7361],{"className":489,"code":7360,"language":491,"meta":141,"style":141},"SELECT * FROM orders WHERE amount BETWEEN 100 AND 200;\n",[77,7362,7363],{"__ignoreMap":141},[495,7364,7365],{"class":497,"line":498},[495,7366,7360],{},[14,7368,7369],{},"快照读：",[21,7371,7372,7375,7378],{},[24,7373,7374],{},"不对查询记录加行锁；",[24,7376,7377],{},"通过 MVCC 读取某个时间点的一致性数据；",[24,7379,7380],{},"其他事务可以继续修改、删除或插入数据。",[14,7382,7383],{},"REPEATABLE READ 下，同一事务中的普通快照读复用同一个 Read View，所以后来提交的记录对当前快照不可见。",[43,7385,7387],{"id":7386},"_2-当前读","2. 当前读",[14,7389,7390],{},"下面这些操作需要读取最新可用的数据，并对数据加锁：",[135,7392,7394],{"className":489,"code":7393,"language":491,"meta":141,"style":141},"SELECT ... FOR UPDATE;\nSELECT ... FOR SHARE;\nUPDATE ...;\nDELETE ...;\n",[77,7395,7396,7401,7405,7410],{"__ignoreMap":141},[495,7397,7398],{"class":497,"line":498},[495,7399,7400],{},"SELECT ... FOR UPDATE;\n",[495,7402,7403],{"class":497,"line":504},[495,7404,7328],{},[495,7406,7407],{"class":497,"line":510},[495,7408,7409],{},"UPDATE ...;\n",[495,7411,7412],{"class":497,"line":661},[495,7413,7414],{},"DELETE ...;\n",[14,7416,7417],{},"它们通常被称为当前读或锁定读。",[14,7419,7420],{},"当前读不能只依靠旧快照，因为业务接下来可能要修改数据。它必须在最新数据状态上进行判断，并锁住相关记录或范围，避免判断完成后数据立即被并发事务改变。",[10,7422,7424],{"id":7423},"五快照读如何避免幻读","五、快照读如何避免幻读",[14,7426,7427],{},"假设当前使用 REPEATABLE READ：",[135,7429,7431],{"className":489,"code":7430,"language":491,"meta":141,"style":141},"START TRANSACTION;\n\nSELECT * FROM orders\nWHERE amount BETWEEN 100 AND 200;\n",[77,7432,7433,7438,7442,7447],{"__ignoreMap":141},[495,7434,7435],{"class":497,"line":498},[495,7436,7437],{},"START TRANSACTION;\n",[495,7439,7440],{"class":497,"line":504},[495,7441,2272],{"emptyLinePlaceholder":1086},[495,7443,7444],{"class":497,"line":510},[495,7445,7446],{},"SELECT * FROM orders\n",[495,7448,7449],{"class":497,"line":661},[495,7450,7451],{},"WHERE amount BETWEEN 100 AND 200;\n",[14,7453,7454,7455,7458],{},"第一次普通查询建立 Read View。此后事务 B 插入一条 ",[77,7456,7457],{},"amount=150"," 的记录并提交：",[135,7460,7462],{"className":489,"code":7461,"language":491,"meta":141,"style":141},"INSERT INTO orders(user_id, amount)\nVALUES (10, 150);\n\nCOMMIT;\n",[77,7463,7464,7469,7474,7478],{"__ignoreMap":141},[495,7465,7466],{"class":497,"line":498},[495,7467,7468],{},"INSERT INTO orders(user_id, amount)\n",[495,7470,7471],{"class":497,"line":504},[495,7472,7473],{},"VALUES (10, 150);\n",[495,7475,7476],{"class":497,"line":510},[495,7477,2272],{"emptyLinePlaceholder":1086},[495,7479,7480],{"class":497,"line":661},[495,7481,7482],{},"COMMIT;\n",[14,7484,7485],{},"事务 A 再次执行相同的普通查询：",[135,7487,7489],{"className":489,"code":7488,"language":491,"meta":141,"style":141},"SELECT * FROM orders\nWHERE amount BETWEEN 100 AND 200;\n",[77,7490,7491,7495],{"__ignoreMap":141},[495,7492,7493],{"class":497,"line":498},[495,7494,7446],{},[495,7496,7497],{"class":497,"line":504},[495,7498,7451],{},[14,7500,7501],{},"由于事务 A 仍然使用原来的 Read View，新插入的记录不在该快照的可见范围内，因此第二次查询不会看到它。",[14,7503,7504],{},"这里需要准确理解：",[47,7506,7507],{},[14,7508,7509],{},"MVCC 没有阻止事务 B 插入记录，只是让这条后来提交的记录对事务 A 的旧快照不可见。",[14,7511,7512],{},"因此，快照读解决的是“当前事务看到什么”，而不是“其他事务能不能写入”。",[10,7514,7516],{"id":7515},"六当前读如何避免幻读","六、当前读如何避免幻读",[14,7518,7519],{},"如果业务需要先锁定一个范围，再根据查询结果修改数据，可以使用：",[135,7521,7523],{"className":489,"code":7522,"language":491,"meta":141,"style":141},"START TRANSACTION;\n\nSELECT * FROM orders\nWHERE amount BETWEEN 100 AND 200\nFOR UPDATE;\n",[77,7524,7525,7529,7533,7537,7542],{"__ignoreMap":141},[495,7526,7527],{"class":497,"line":498},[495,7528,7437],{},[495,7530,7531],{"class":497,"line":504},[495,7532,2272],{"emptyLinePlaceholder":1086},[495,7534,7535],{"class":497,"line":510},[495,7536,7446],{},[495,7538,7539],{"class":497,"line":661},[495,7540,7541],{},"WHERE amount BETWEEN 100 AND 200\n",[495,7543,7544],{"class":497,"line":2251},[495,7545,7546],{},"FOR UPDATE;\n",[14,7548,7549,7550,7553],{},"假设 ",[77,7551,7552],{},"amount"," 上存在普通索引：",[135,7555,7557],{"className":489,"code":7556,"language":491,"meta":141,"style":141},"CREATE INDEX idx_orders_amount ON orders(amount);\n",[77,7558,7559],{"__ignoreMap":141},[495,7560,7561],{"class":497,"line":498},[495,7562,7556],{},[14,7564,7565],{},"在 REPEATABLE READ 下，InnoDB 扫描这个索引范围时，通常会对索引记录及其间隙加 Next-Key Lock。",[14,7567,7568],{},"此时另一个事务尝试插入：",[135,7570,7572],{"className":489,"code":7571,"language":491,"meta":141,"style":141},"INSERT INTO orders(user_id, amount)\nVALUES (10, 150);\n",[77,7573,7574,7578],{"__ignoreMap":141},[495,7575,7576],{"class":497,"line":498},[495,7577,7468],{},[495,7579,7580],{"class":497,"line":504},[495,7581,7473],{},[14,7583,7584,7585,7588],{},"由于 ",[77,7586,7587],{},"150"," 落在已经锁定的索引范围内，该插入通常会被阻塞，直到前一个事务提交或回滚。",[14,7590,7591],{},"这次不是“让新记录不可见”，而是：",[47,7593,7594],{},[14,7595,7596],{},"阻止新记录进入已经锁定的查询范围。",[10,7598,7600],{"id":7599},"七record-lockgap-lock-与-next-key-lock","七、Record Lock、Gap Lock 与 Next-Key Lock",[43,7602,7604],{"id":7603},"_1-record-lock","1. Record Lock",[14,7606,7607],{},"Record Lock 锁住的是索引记录。",[14,7609,106],{},[135,7611,7613],{"className":489,"code":7612,"language":491,"meta":141,"style":141},"SELECT * FROM orders\nWHERE id = 100\nFOR UPDATE;\n",[77,7614,7615,7619,7623],{"__ignoreMap":141},[495,7616,7617],{"class":497,"line":498},[495,7618,7446],{},[495,7620,7621],{"class":497,"line":504},[495,7622,626],{},[495,7624,7625],{"class":497,"line":510},[495,7626,7546],{},[14,7628,797,7629,7632],{},[77,7630,7631],{},"id"," 是唯一索引，并且使用唯一等值条件准确定位一条记录，InnoDB 通常只锁找到的索引记录，不需要锁前面的间隙。",[43,7634,7636],{"id":7635},"_2-gap-lock","2. Gap Lock",[14,7638,7639],{},"Gap Lock 锁住的是两个索引记录之间的间隙，而不是某一条已经存在的记录。",[14,7641,7642],{},"假设索引中已有：",[135,7644,7647],{"className":7645,"code":7646,"language":140,"meta":141},[138],"100\n200\n300\n",[77,7648,7646],{"__ignoreMap":141},[14,7650,7651],{},"那么其中存在：",[135,7653,7656],{"className":7654,"code":7655,"language":140,"meta":141},[138],"(-∞, 100)\n(100, 200)\n(200, 300)\n(300, +∞)\n",[77,7657,7655],{"__ignoreMap":141},[14,7659,7660],{},"Gap Lock 的主要作用是限制其他事务在对应间隙插入新索引记录。",[43,7662,7664],{"id":7663},"_3-next-key-lock","3. Next-Key Lock",[14,7666,7667],{},"Next-Key Lock 的准确定义是：",[135,7669,7672],{"className":7670,"code":7671,"language":140,"meta":141},[138],"Next-Key Lock = Record Lock + 该记录前面的 Gap Lock\n",[77,7673,7671],{"__ignoreMap":141},[14,7675,7676],{},"这里的“前面”是指索引排序中的前一个间隙。假设索引值为：",[135,7678,7681],{"className":7679,"code":7680,"language":140,"meta":141},[138],"3\n5\n8\n",[77,7682,7680],{"__ignoreMap":141},[14,7684,7685],{},"可能形成的 Next-Key Lock 区间是：",[135,7687,7690],{"className":7688,"code":7689,"language":140,"meta":141},[138],"(-∞, 3]\n(3, 5]\n(5, 8]\n(8, +∞)\n",[77,7691,7689],{"__ignoreMap":141},[14,7693,7694,7695,7698,7699,7702,7703,7705,7706,5142,7709,7711],{},"例如，对索引记录 ",[77,7696,7697],{},"5"," 加 Next-Key Lock，可以概念性地理解为锁住 ",[77,7700,7701],{},"(3, 5]","：既保护记录 ",[77,7704,7697],{},"，又阻止其他事务向 ",[77,7707,7708],{},"3",[77,7710,7697],{}," 之间插入新索引记录。",[14,7713,7714],{},"这里需要区分单个 Next-Key Lock 和一条 SQL 最终获得的整组锁：",[47,7716,7717],{},[14,7718,7719],{},"一个 Next-Key Lock 只包含当前记录和它前面的间隙，但一条 SQL 可能同时获得多个 Record Lock、Gap Lock 和 Next-Key Lock，最终覆盖查询记录前后的整个范围。",[14,7721,7722],{},"例如，在 REPEATABLE READ 下执行：",[135,7724,7726],{"className":489,"code":7725,"language":491,"meta":141,"style":141},"SELECT * FROM orders\nWHERE amount = 5\nFOR UPDATE;\n",[77,7727,7728,7732,7737],{"__ignoreMap":141},[495,7729,7730],{"class":497,"line":498},[495,7731,7446],{},[495,7733,7734],{"class":497,"line":504},[495,7735,7736],{},"WHERE amount = 5\n",[495,7738,7739],{"class":497,"line":510},[495,7740,7546],{},[14,7742,797,7743,7745,7746,7749,7750,7753],{},[77,7744,7552],{}," 是普通非唯一索引，并且相邻索引值为 ",[77,7747,7748],{},"3、5、8","，为了防止其他事务再次插入 ",[77,7751,7752],{},"amount=5"," 的记录，最终需要保护：",[135,7755,7758],{"className":7756,"code":7757,"language":140,"meta":141},[138],"记录 5\n记录 5 前面的间隙：(3, 5)\n记录 5 后面的间隙：(5, 8)\n",[77,7759,7757],{"__ignoreMap":141},[14,7761,7762],{},"可以概念性地理解为：",[135,7764,7767],{"className":7765,"code":7766,"language":140,"meta":141},[138],"(3, 5]  → 记录 5 的 Next-Key Lock\n(5, 8)  → 查询范围结束位置的 Gap Lock\n",[77,7768,7766],{"__ignoreMap":141},[14,7770,7771],{},"所以，“等值查询可能锁住匹配记录及其前后间隙”是对一条 SQL 最终锁定范围的描述；它不代表单个 Next-Key Lock 同时包含记录前后两个间隙。",[14,7773,7774],{},"还要区分以下情况：",[21,7776,7777,7782,7793,7796],{},[24,7778,7023,7779,7781],{},[77,7780,7026],{}," 是快照读，不会因为这个查询条件加 Next-Key Lock。",[24,7783,7784,220,7786,220,7788,1955,7790,7792],{},[77,7785,7001],{},[77,7787,7004],{},[77,7789,976],{},[77,7791,979],{}," 才属于锁定读或当前读。",[24,7794,7795],{},"如果使用唯一索引和唯一等值条件，并且准确找到已有记录，InnoDB 通常只加 Record Lock，不需要锁前后间隙。",[24,7797,7798],{},"如果查询条件没有命中唯一记录，或者使用普通非唯一索引，InnoDB 会根据实际扫描范围使用 Gap Lock 或 Next-Key Lock。",[14,7800,7801,7802,7805],{},"具体获得哪些锁，最终还取决于隔离级别、索引类型、查询条件和实际执行计划，不能只根据 ",[77,7803,7804],{},"WHERE"," 条件判断。",[14,7807,7808],{},"InnoDB 还可以锁住最后一条记录之后的间隙，从而保护开放区间查询：",[135,7810,7812],{"className":489,"code":7811,"language":491,"meta":141,"style":141},"SELECT * FROM orders\nWHERE amount > 200\nFOR UPDATE;\n",[77,7813,7814,7818,7823],{"__ignoreMap":141},[495,7815,7816],{"class":497,"line":498},[495,7817,7446],{},[495,7819,7820],{"class":497,"line":504},[495,7821,7822],{},"WHERE amount > 200\n",[495,7824,7825],{"class":497,"line":510},[495,7826,7546],{},[10,7828,7830],{"id":7829},"八索引为什么很重要","八、索引为什么很重要",[14,7832,7833],{},"InnoDB 的行锁本质上是索引记录锁，Next-Key Lock 也是围绕索引扫描范围建立的。",[14,7835,7836],{},"如果查询能够使用合适索引：",[135,7838,7840],{"className":489,"code":7839,"language":491,"meta":141,"style":141},"SELECT * FROM orders\nWHERE amount BETWEEN 100 AND 200\nFOR UPDATE;\n",[77,7841,7842,7846,7850],{"__ignoreMap":141},[495,7843,7844],{"class":497,"line":498},[495,7845,7446],{},[495,7847,7848],{"class":497,"line":504},[495,7849,7541],{},[495,7851,7852],{"class":497,"line":510},[495,7853,7546],{},[14,7855,7856],{},"InnoDB 可以更准确地锁定相关索引范围。",[14,7858,7859],{},"如果缺少合适索引，InnoDB 可能需要扫描并锁定大量索引记录，锁的范围会变大，并发能力明显下降。",[14,7861,7862],{},"这里不要简单表述为：",[47,7864,7865],{},[14,7866,7867],{},"没有索引就一定加表锁。",[14,7869,1599],{},[47,7871,7872],{},[14,7873,7874],{},"InnoDB 仍然基于索引记录加锁；缺少可用业务索引时，可能扫描并锁住大量记录，使效果接近大范围锁定。",[14,7876,7877],{},"因此，使用锁定读避免幻读时，需要同时关注：",[21,7879,7880,7883,7886,7889,7892],{},[24,7881,7882],{},"查询条件是否命中索引；",[24,7884,7885],{},"实际执行计划选择了哪个索引；",[24,7887,7888],{},"锁定范围是否超出预期；",[24,7890,7891],{},"事务是否足够短；",[24,7893,7894],{},"是否可能形成死锁。",[10,7896,7898],{"id":7897},"九为什么不能只依赖快照读完成先查后写","九、为什么不能只依赖快照读完成“先查后写”",[14,7900,7901],{},"假设业务要求同一用户只能存在一条有效申请：",[135,7903,7905],{"className":489,"code":7904,"language":491,"meta":141,"style":141},"SELECT * FROM applications\nWHERE user_id = 100\n  AND status = 'ACTIVE';\n",[77,7906,7907,7912,7917],{"__ignoreMap":141},[495,7908,7909],{"class":497,"line":498},[495,7910,7911],{},"SELECT * FROM applications\n",[495,7913,7914],{"class":497,"line":504},[495,7915,7916],{},"WHERE user_id = 100\n",[495,7918,7919],{"class":497,"line":510},[495,7920,7921],{},"  AND status = 'ACTIVE';\n",[14,7923,7924],{},"两个事务可能同时通过快照读发现“记录不存在”，然后各自插入一条有效申请。",[14,7926,7927],{},"REPEATABLE READ 虽然让两个事务各自的查询结果保持一致，但没有阻止另一个事务执行插入。",[14,7929,7930],{},"因此，业务不变量不能只依赖普通快照读。常见方案是：",[705,7932,7933,7936,7939,7942],{},[24,7934,7935],{},"建立能够表达业务规则的唯一索引；",[24,7937,7938],{},"在 REPEATABLE READ 下使用合适的锁定读；",[24,7940,7941],{},"捕获重复键或死锁异常，并进行有限重试；",[24,7943,7944],{},"缩短事务时间，避免锁范围长期占用。",[14,7946,7947],{},"例如业务规则能够转换为唯一键时，优先让数据库约束兜底：",[135,7949,7951],{"className":489,"code":7950,"language":491,"meta":141,"style":141},"CREATE UNIQUE INDEX uk_user_active\nON applications(user_id, active_flag);\n",[77,7952,7953,7958],{"__ignoreMap":141},[495,7954,7955],{"class":497,"line":498},[495,7956,7957],{},"CREATE UNIQUE INDEX uk_user_active\n",[495,7959,7960],{"class":497,"line":504},[495,7961,7962],{},"ON applications(user_id, active_flag);\n",[14,7964,7965],{},"具体索引设计仍要根据数据模型确定，不能为了唯一约束直接照搬示例。",[10,7967,7969],{"id":7968},"十混用快照读和当前读为什么容易困惑","十、混用快照读和当前读为什么容易困惑",[14,7971,7972,7973,7975],{},"在 REPEATABLE READ 事务中，普通 ",[77,7974,7026],{}," 使用旧快照，而锁定读读取最新可用的数据。",[14,7977,7978],{},"可能出现：",[135,7980,7983],{"className":7981,"code":7982,"language":140,"meta":141},[138],"1. 普通 SELECT 没看到某条新记录\n2. 另一个事务插入该记录并提交\n3. SELECT ... FOR UPDATE 却看到了这条记录\n",[77,7984,7982],{"__ignoreMap":141},[14,7986,7987],{},"原因不是隔离级别突然失效，而是两条语句使用了不同的读取机制：",[21,7989,7990,7995],{},[24,7991,7023,7992,7994],{},[77,7993,7026],{},"：读取 Read View 中的一致性快照；",[24,7996,7997,7999],{},[77,7998,7001],{},"：读取最新可用数据并加锁。",[14,8001,8002],{},"MySQL 官方文档也不建议在同一个 REPEATABLE READ 事务中随意混用锁定语句和非锁定查询，因为它们可能呈现两个不同时间状态的数据。",[14,8004,8005],{},"如果业务必须基于最新结果做写入判断，应统一使用锁定读；如果必须获得更严格、容易理解的串行语义，可以评估 SERIALIZABLE，但要接受更高的锁竞争。",[10,8007,8009],{"id":8008},"十一read-committed-下加-for-update-能否避免幻读","十一、READ COMMITTED 下加 FOR UPDATE 能否避免幻读",[14,8011,8012],{},"不能笼统地说可以。",[14,8014,8015,8016,1955,8018,8020],{},"在 READ COMMITTED 下，InnoDB 对锁定读、",[77,8017,976],{},[77,8019,979],{}," 通常只锁索引记录，不锁前面的间隙。其他事务仍可以向记录间隙插入新行，因此范围查询仍可能出现幻读。",[14,8022,8023],{},"Gap Lock 在该级别主要保留给：",[21,8025,8026,8029],{},[24,8027,8028],{},"外键约束检查；",[24,8030,8031],{},"重复键检查。",[14,8033,8034],{},"如果业务需要锁住一个尚不存在的范围，应评估：",[21,8036,8037,8040,8043,8046],{},[24,8038,8039],{},"使用 REPEATABLE READ，并通过合适索引和 Next-Key Lock 锁定范围；",[24,8041,8042],{},"使用 SERIALIZABLE；",[24,8044,8045],{},"把业务不变量转换为唯一约束；",[24,8047,8048],{},"调整数据模型，避免依赖无法锁定的“空结果”。",[10,8050,8052],{"id":8051},"十二如何选择隔离级别","十二、如何选择隔离级别",[43,8054,8056],{"id":8055},"选择-read-committed","选择 READ COMMITTED",[14,8058,3169],{},[21,8060,8061,8064,8067,8070],{},[24,8062,8063],{},"希望每次查询都看到最新已提交数据；",[24,8065,8066],{},"可以接受同一事务内两次读取结果不同；",[24,8068,8069],{},"希望减少 Gap Lock 和锁冲突；",[24,8071,8072],{},"业务通过版本号、唯一约束或其他机制保证并发正确性。",[43,8074,8076],{"id":8075},"选择-repeatable-read","选择 REPEATABLE READ",[14,8078,3169],{},[21,8080,8081,8084,8087,8090],{},[24,8082,8083],{},"希望事务中的普通查询保持一致视图；",[24,8085,8086],{},"需要使用 Next-Key Lock 保护范围；",[24,8088,8089],{},"能够控制索引、锁顺序和事务时长；",[24,8091,8092],{},"接受 InnoDB 默认隔离级别。",[43,8094,8096],{"id":8095},"选择-serializable","选择 SERIALIZABLE",[14,8098,3169],{},[21,8100,8101,8104,8107,8110],{},[24,8102,8103],{},"并发量较低；",[24,8105,8106],{},"一致性要求极高；",[24,8108,8109],{},"业务逻辑难以通过更细粒度约束实现；",[24,8111,8112],{},"能够接受更多阻塞、超时和死锁重试。",[14,8114,8115],{},"隔离级别不是越高越好。选择时需要综合评估：",[21,8117,8118,8121,8124,8127,8130,8133],{},[24,8119,8120],{},"一致性要求；",[24,8122,8123],{},"读写比例；",[24,8125,8126],{},"热点数据；",[24,8128,8129],{},"事务持续时间；",[24,8131,8132],{},"可接受的锁等待；",[24,8134,8135],{},"数据库吞吐量。",[10,8137,8139],{"id":8138},"十三查看和设置隔离级别","十三、查看和设置隔离级别",[14,8141,8142],{},"查看当前会话隔离级别：",[135,8144,8146],{"className":489,"code":8145,"language":491,"meta":141,"style":141},"SELECT @@SESSION.transaction_isolation;\n",[77,8147,8148],{"__ignoreMap":141},[495,8149,8150],{"class":497,"line":498},[495,8151,8145],{},[14,8153,8154],{},"查看全局默认隔离级别：",[135,8156,8158],{"className":489,"code":8157,"language":491,"meta":141,"style":141},"SELECT @@GLOBAL.transaction_isolation;\n",[77,8159,8160],{"__ignoreMap":141},[495,8161,8162],{"class":497,"line":498},[495,8163,8157],{},[14,8165,8166],{},"修改当前会话后续事务的隔离级别：",[135,8168,8170],{"className":489,"code":8169,"language":491,"meta":141,"style":141},"SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;\n",[77,8171,8172],{"__ignoreMap":141},[495,8173,8174],{"class":497,"line":498},[495,8175,8169],{},[14,8177,8178],{},"设置下一个事务的隔离级别：",[135,8180,8182],{"className":489,"code":8181,"language":491,"meta":141,"style":141},"SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;\n",[77,8183,8184],{"__ignoreMap":141},[495,8185,8186],{"class":497,"line":498},[495,8187,8181],{},[14,8189,8190],{},"开始事务：",[135,8192,8193],{"className":489,"code":7437,"language":491,"meta":141,"style":141},[77,8194,8195],{"__ignoreMap":141},[495,8196,8197],{"class":497,"line":498},[495,8198,7437],{},[14,8200,8201],{},"线上修改隔离级别前，需要：",[21,8203,8204,8207,8214],{},[24,8205,8206],{},"验证依赖一致性快照、锁定范围和“先查后写”的业务逻辑；",[24,8208,8209,8210,8213],{},"确认 ",[77,8211,8212],{},"binlog_format"," 与目标隔离级别兼容；",[24,8215,8216],{},"观察切换前后的锁等待、死锁率、事务时延和数据库吞吐量。",[14,8218,8219,8220,8223,8224,8227],{},"这里需要确认的不是笼统的“复制方式”，而是二进制日志格式的兼容性。例如 MySQL 的 READ COMMITTED 只支持 Row-Based Logging；如果配置为 ",[77,8221,8222],{},"MIXED","，MySQL 会自动使用行格式记录。系统原本已经使用 ",[77,8225,8226],{},"ROW"," 时，通常不需要为此额外调整。",[14,8229,8230],{},"监控指标也不是选择隔离级别的前置条件，而是用于验证切换产生的实际影响：",[21,8232,8233,8236],{},[24,8234,8235],{},"从 REPEATABLE READ 切换到 READ COMMITTED，Gap Lock 通常减少，锁等待和死锁概率可能下降，但业务会失去事务级固定快照；",[24,8237,8238],{},"切换到更高隔离级别时，锁范围和等待时间可能增加，事务耗时及吞吐量可能受到影响。",[14,8240,8241],{},"因此，不能只根据理论吞吐量直接切换隔离级别。",[10,8243,3332],{"id":3331},[43,8245,8247],{"id":8246},"误区-1mvcc-会阻止其他事务插入","误区 1：MVCC 会阻止其他事务插入",[14,8249,8250],{},"不会。MVCC 主要决定一条记录对当前 Read View 是否可见，不会因为快照读而阻塞其他事务写入。",[43,8252,8254],{"id":8253},"误区-2repeatable-read-只靠-mvcc-避免所有幻读","误区 2：REPEATABLE READ 只靠 MVCC 避免所有幻读",[14,8256,8257],{},"不完整。",[21,8259,8260,8263],{},[24,8261,8262],{},"快照读靠 MVCC 和固定 Read View；",[24,8264,8265],{},"当前读靠 Gap Lock 和 Next-Key Lock。",[43,8267,8269],{"id":8268},"误区-3只要加-for-update-就一定不会幻读","误区 3：只要加 FOR UPDATE 就一定不会幻读",[14,8271,8272],{},"不一定。是否锁住间隙与隔离级别、查询条件、索引类型和实际扫描范围有关。READ COMMITTED 下通常不会为普通范围查询使用 Gap Lock。",[43,8274,8276],{"id":8275},"误区-4next-key-lock-锁的是数据页","误区 4：Next-Key Lock 锁的是数据页",[14,8278,8279],{},"不是。它围绕索引记录和索引记录之间的间隙建立。",[43,8281,8283],{"id":8282},"误区-5没有索引就一定升级为表锁","误区 5：没有索引就一定升级为表锁",[14,8285,8286],{},"这个表述不准确。InnoDB 仍然基于索引进行记录锁定，但缺少合适索引可能导致扫描和锁定大量记录，使并发效果接近大范围锁定。",[43,8288,8290],{"id":8289},"误区-6serializable-一定是最好的选择","误区 6：SERIALIZABLE 一定是最好的选择",[14,8292,8293],{},"它的隔离性最强，但会显著增加锁等待，降低并发能力。多数业务更适合通过合理隔离级别、索引、锁定读和数据库约束共同保证正确性。",[10,8295,1007],{"id":1007},[21,8297,8298,8305,8312,8319,8326],{},[24,8299,8300],{},[58,8301,8304],{"href":8302,"rel":8303},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-transaction-isolation-levels.html",[1016],"MySQL 8.4 Reference Manual：Transaction Isolation Levels",[24,8306,8307],{},[58,8308,8311],{"href":8309,"rel":8310},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-consistent-read.html",[1016],"MySQL 8.4 Reference Manual：Consistent Nonlocking Reads",[24,8313,8314],{},[58,8315,8318],{"href":8316,"rel":8317},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-next-key-locking.html",[1016],"MySQL 8.4 Reference Manual：Phantom Rows",[24,8320,8321],{},[58,8322,8325],{"href":8323,"rel":8324},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-locking-reads.html",[1016],"MySQL 8.4 Reference Manual：Locking Reads",[24,8327,8328],{},[58,8329,8332],{"href":8330,"rel":8331},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Fset-transaction.html",[1016],"MySQL 8.4 Reference Manual：SET TRANSACTION Statement",[1047,8334,1049],{},{"title":141,"searchDepth":504,"depth":504,"links":8336},[8337,8340,8341,8342,8347,8353,8357,8358,8359,8364,8365,8366,8367,8368,8373,8374,8382],{"id":12,"depth":504,"text":12,"children":8338},[8339],{"id":45,"depth":510,"text":45},{"id":54,"depth":504,"text":54},{"id":7047,"depth":504,"text":7048},{"id":7153,"depth":504,"text":7154,"children":8343},[8344,8345,8346],{"id":7157,"depth":510,"text":7158},{"id":7176,"depth":510,"text":7177},{"id":7197,"depth":510,"text":7198},{"id":7232,"depth":504,"text":7233,"children":8348},[8349,8350,8351,8352],{"id":7236,"depth":510,"text":7237},{"id":7257,"depth":510,"text":7258},{"id":7290,"depth":510,"text":7291},{"id":7315,"depth":510,"text":7316},{"id":7343,"depth":504,"text":7344,"children":8354},[8355,8356],{"id":7350,"depth":510,"text":7351},{"id":7386,"depth":510,"text":7387},{"id":7423,"depth":504,"text":7424},{"id":7515,"depth":504,"text":7516},{"id":7599,"depth":504,"text":7600,"children":8360},[8361,8362,8363],{"id":7603,"depth":510,"text":7604},{"id":7635,"depth":510,"text":7636},{"id":7663,"depth":510,"text":7664},{"id":7829,"depth":504,"text":7830},{"id":7897,"depth":504,"text":7898},{"id":7968,"depth":504,"text":7969},{"id":8008,"depth":504,"text":8009},{"id":8051,"depth":504,"text":8052,"children":8369},[8370,8371,8372],{"id":8055,"depth":510,"text":8056},{"id":8075,"depth":510,"text":8076},{"id":8095,"depth":510,"text":8096},{"id":8138,"depth":504,"text":8139},{"id":3331,"depth":504,"text":3332,"children":8375},[8376,8377,8378,8379,8380,8381],{"id":8246,"depth":510,"text":8247},{"id":8253,"depth":510,"text":8254},{"id":8268,"depth":510,"text":8269},{"id":8275,"depth":510,"text":8276},{"id":8282,"depth":510,"text":8283},{"id":8289,"depth":510,"text":8290},{"id":1007,"depth":504,"text":1007},"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":6971,"description":8384},"java\u002Fmysql-transaction-isolation-and-phantom-read","2yviidjMyp_4WXSlt4NyCkf-cVrM_t7Cr0mPuaifjnE",{"id":4,"title":5,"body":8392,"commentId":1080,"description":1081,"difficulty":1082,"draft":1083,"extension":1084,"meta":9133,"navigation":1086,"order":1087,"path":1088,"section":1089,"seo":9134,"stem":1091,"updated":1092,"__hash__":1093},{"type":7,"value":8393,"toc":9103},[8394,8396,8398,8400,8410,8412,8414,8416,8420,8422,8429,8431,8435,8449,8451,8453,8463,8465,8467,8469,8471,8476,8478,8490,8494,8496,8498,8500,8505,8507,8519,8521,8529,8531,8609,8611,8613,8615,8617,8619,8621,8626,8628,8632,8636,8638,8640,8644,8646,8650,8652,8657,8659,8664,8666,8668,8670,8672,8677,8679,8681,8683,8688,8690,8695,8697,8701,8708,8710,8712,8728,8732,8737,8739,8744,8746,8748,8780,8782,8784,8786,8788,8793,8795,8799,8801,8803,8811,8813,8815,8835,8837,8839,8851,8853,8855,8857,8859,8861,8871,8873,8878,8880,8888,8890,8892,8894,8896,8901,8903,8911,8913,8929,8933,8938,8940,8942,8958,8960,8967,8969,8971,8973,8978,8980,8982,8984,8986,8998,9000,9002,9004,9006,9011,9013,9015,9025,9027,9029,9031,9033,9035,9037,9039,9041,9043,9045,9047,9055,9057,9059,9064,9068,9072,9074,9101],[10,8395,12],{"id":12},[14,8397,16],{},[14,8399,19],{},[21,8401,8402,8404,8406,8408],{},[24,8403,26],{},[24,8405,29],{},[24,8407,32],{},[24,8409,35],{},[14,8411,38],{},[14,8413,41],{},[43,8415,45],{"id":45},[47,8417,8418],{},[14,8419,51],{},[10,8421,54],{"id":54},[14,8423,8424],{},[58,8425,8427],{"href":60,"target":61,"rel":8426},[63,64],[66,8428],{"src":60,"alt":68},[10,8430,72],{"id":71},[14,8432,75,8433,80],{},[77,8434,79],{},[21,8436,8437,8439,8441,8443,8445,8447],{},[24,8438,85],{},[24,8440,88],{},[24,8442,91],{},[24,8444,94],{},[24,8446,97],{},[24,8448,100],{},[14,8450,103],{},[14,8452,106],{},[21,8454,8455,8457,8459,8461],{},[24,8456,111],{},[24,8458,114],{},[24,8460,117],{},[24,8462,120],{},[10,8464,124],{"id":123},[14,8466,127],{},[14,8468,130],{},[14,8470,133],{},[135,8472,8474],{"className":8473,"code":139,"language":140,"meta":141},[138],[77,8475,139],{"__ignoreMap":141},[14,8477,146],{},[21,8479,8480,8482,8484,8486,8488],{},[24,8481,151],{},[24,8483,154],{},[24,8485,157],{},[24,8487,160],{},[24,8489,163],{},[14,8491,166,8492,170],{},[77,8493,169],{},[10,8495,174],{"id":173},[14,8497,177],{},[14,8499,180],{},[135,8501,8503],{"className":8502,"code":184,"language":140,"meta":141},[138],[77,8504,184],{"__ignoreMap":141},[14,8506,189],{},[21,8508,8509,8511,8513,8515,8517],{},[24,8510,194],{},[24,8512,197],{},[24,8514,200],{},[24,8516,97],{},[24,8518,205],{},[14,8520,208],{},[14,8522,211,8523,216,8525,220,8527,224],{},[213,8524,215],{},[77,8526,219],{},[77,8528,223],{},[10,8530,228],{"id":227},[230,8532,8533,8543],{},[233,8534,8535],{},[236,8536,8537,8539,8541],{},[239,8538,241],{},[239,8540,244],{},[239,8542,247],{},[249,8544,8545,8553,8561,8569,8577,8585,8593,8601],{},[236,8546,8547,8549,8551],{},[254,8548,256],{},[254,8550,259],{},[254,8552,262],{},[236,8554,8555,8557,8559],{},[254,8556,267],{},[254,8558,270],{},[254,8560,273],{},[236,8562,8563,8565,8567],{},[254,8564,278],{},[254,8566,281],{},[254,8568,284],{},[236,8570,8571,8573,8575],{},[254,8572,289],{},[254,8574,292],{},[254,8576,295],{},[236,8578,8579,8581,8583],{},[254,8580,300],{},[254,8582,303],{},[254,8584,306],{},[236,8586,8587,8589,8591],{},[254,8588,311],{},[254,8590,314],{},[254,8592,317],{},[236,8594,8595,8597,8599],{},[254,8596,322],{},[254,8598,325],{},[254,8600,328],{},[236,8602,8603,8605,8607],{},[254,8604,333],{},[254,8606,336],{},[254,8608,339],{},[14,8610,342],{},[14,8612,345],{},[10,8614,349],{"id":348},[14,8616,352],{},[14,8618,355],{},[14,8620,358],{},[135,8622,8624],{"className":8623,"code":362,"language":140,"meta":141},[138],[77,8625,362],{"__ignoreMap":141},[14,8627,367],{},[14,8629,370,8630,374],{},[77,8631,373],{},[47,8633,8634],{},[14,8635,379],{},[10,8637,383],{"id":382},[14,8639,386],{},[47,8641,8642],{},[14,8643,391],{},[14,8645,394],{},[14,8647,397,8648,401],{},[213,8649,400],{},[14,8651,404],{},[135,8653,8655],{"className":8654,"code":408,"language":140,"meta":141},[138],[77,8656,408],{"__ignoreMap":141},[14,8658,413],{},[135,8660,8662],{"className":8661,"code":417,"language":140,"meta":141},[138],[77,8663,417],{"__ignoreMap":141},[14,8665,422],{},[10,8667,426],{"id":425},[14,8669,429],{},[14,8671,432],{},[135,8673,8675],{"className":8674,"code":436,"language":140,"meta":141},[138],[77,8676,436],{"__ignoreMap":141},[14,8678,441],{},[14,8680,444],{},[14,8682,447],{},[135,8684,8686],{"className":8685,"code":451,"language":140,"meta":141},[138],[77,8687,451],{"__ignoreMap":141},[14,8689,456],{},[135,8691,8693],{"className":8692,"code":460,"language":140,"meta":141},[138],[77,8694,460],{"__ignoreMap":141},[14,8696,465],{},[47,8698,8699],{},[14,8700,470],{},[14,8702,8703],{},[58,8704,8706],{"href":475,"target":61,"rel":8705},[63,64],[66,8707],{"src":475,"alt":479},[10,8709,483],{"id":482},[14,8711,486],{},[135,8713,8714],{"className":489,"code":490,"language":491,"meta":141,"style":141},[77,8715,8716,8720,8724],{"__ignoreMap":141},[495,8717,8718],{"class":497,"line":498},[495,8719,501],{},[495,8721,8722],{"class":497,"line":504},[495,8723,507],{},[495,8725,8726],{"class":497,"line":510},[495,8727,513],{},[14,8729,516,8730,520],{},[77,8731,519],{},[135,8733,8735],{"className":8734,"code":524,"language":140,"meta":141},[138],[77,8736,524],{"__ignoreMap":141},[14,8738,529],{},[135,8740,8742],{"className":8741,"code":533,"language":140,"meta":141},[138],[77,8743,533],{"__ignoreMap":141},[14,8745,538],{},[14,8747,541],{},[21,8749,8750,8760,8764,8768,8772,8778],{},[24,8751,8752,220,8754,220,8756,220,8758,558],{},[77,8753,548],{},[77,8755,551],{},[77,8757,554],{},[77,8759,557],{},[24,8761,8762,558],{},[77,8763,563],{},[24,8765,566,8766,558],{},[77,8767,569],{},[24,8769,572,8770,558],{},[77,8771,575],{},[24,8773,578,8774,220,8776,558],{},[77,8775,581],{},[77,8777,584],{},[24,8779,587],{},[14,8781,590],{},[10,8783,594],{"id":593},[14,8785,597],{},[14,8787,600],{},[135,8789,8791],{"className":8790,"code":604,"language":140,"meta":141},[138],[77,8792,604],{"__ignoreMap":141},[14,8794,609],{},[14,8796,612,8797,616],{},[77,8798,615],{},[10,8800,620],{"id":619},[14,8802,623],{},[135,8804,8805],{"className":489,"code":626,"language":491,"meta":141,"style":141},[77,8806,8807],{"__ignoreMap":141},[495,8808,8809],{"class":497,"line":498},[495,8810,626],{},[14,8812,635],{},[14,8814,638],{},[135,8816,8817],{"className":489,"code":641,"language":491,"meta":141,"style":141},[77,8818,8819,8823,8827,8831],{"__ignoreMap":141},[495,8820,8821],{"class":497,"line":498},[495,8822,648],{},[495,8824,8825],{"class":497,"line":504},[495,8826,653],{},[495,8828,8829],{"class":497,"line":510},[495,8830,658],{},[495,8832,8833],{"class":497,"line":661},[495,8834,664],{},[14,8836,667],{},[14,8838,670],{},[21,8840,8841,8843,8845,8847,8849],{},[24,8842,675],{},[24,8844,678],{},[24,8846,681],{},[24,8848,684],{},[24,8850,687],{},[14,8852,690],{},[14,8854,693],{},[10,8856,697],{"id":696},[14,8858,700],{},[14,8860,703],{},[705,8862,8863,8865,8869],{},[24,8864,709],{},[24,8866,712,8867,716],{},[77,8868,715],{},[24,8870,719],{},[14,8872,722],{},[135,8874,8876],{"className":8875,"code":726,"language":140,"meta":141},[138],[77,8877,726],{"__ignoreMap":141},[14,8879,731],{},[135,8881,8882],{"className":489,"code":734,"language":491,"meta":141,"style":141},[77,8883,8884],{"__ignoreMap":141},[495,8885,8886],{"class":497,"line":498},[495,8887,734],{},[14,8889,743],{},[14,8891,746],{},[10,8893,750],{"id":749},[14,8895,753],{},[135,8897,8899],{"className":8898,"code":757,"language":140,"meta":141},[138],[77,8900,757],{"__ignoreMap":141},[14,8902,762],{},[135,8904,8905],{"className":489,"code":765,"language":491,"meta":141,"style":141},[77,8906,8907],{"__ignoreMap":141},[495,8908,8909],{"class":497,"line":498},[495,8910,765],{},[14,8912,774],{},[135,8914,8915],{"className":489,"code":777,"language":491,"meta":141,"style":141},[77,8916,8917,8921,8925],{"__ignoreMap":141},[495,8918,8919],{"class":497,"line":498},[495,8920,784],{},[495,8922,8923],{"class":497,"line":504},[495,8924,789],{},[495,8926,8927],{"class":497,"line":510},[495,8928,794],{},[14,8930,797,8931,801],{},[77,8932,800],{},[135,8934,8936],{"className":8935,"code":805,"language":140,"meta":141},[138],[77,8937,805],{"__ignoreMap":141},[14,8939,810],{},[14,8941,813],{},[135,8943,8944],{"className":489,"code":816,"language":491,"meta":141,"style":141},[77,8945,8946,8950,8954],{"__ignoreMap":141},[495,8947,8948],{"class":497,"line":498},[495,8949,823],{},[495,8951,8952],{"class":497,"line":504},[495,8953,789],{},[495,8955,8956],{"class":497,"line":510},[495,8957,794],{},[14,8959,834],{},[14,8961,8962],{},[58,8963,8965],{"href":839,"target":61,"rel":8964},[63,64],[66,8966],{"src":839,"alt":843},[10,8968,847],{"id":846},[14,8970,850],{},[14,8972,853],{},[135,8974,8976],{"className":8975,"code":857,"language":140,"meta":141},[138],[77,8977,857],{"__ignoreMap":141},[14,8979,862],{},[14,8981,865],{},[10,8983,869],{"id":868},[14,8985,872],{},[21,8987,8988,8990,8992,8994,8996],{},[24,8989,877],{},[24,8991,880],{},[24,8993,883],{},[24,8995,886],{},[24,8997,889],{},[14,8999,892],{},[10,9001,896],{"id":895},[14,9003,899],{},[14,9005,902],{},[135,9007,9009],{"className":9008,"code":906,"language":140,"meta":141},[138],[77,9010,906],{"__ignoreMap":141},[14,9012,911],{},[14,9014,914],{},[21,9016,9017,9019,9021,9023],{},[24,9018,919],{},[24,9020,922],{},[24,9022,925],{},[24,9024,928],{},[14,9026,931],{},[10,9028,935],{"id":934},[43,9030,939],{"id":938},[14,9032,942],{},[43,9034,946],{"id":945},[14,9036,949],{},[43,9038,953],{"id":952},[14,9040,956],{},[43,9042,960],{"id":959},[14,9044,963],{},[43,9046,967],{"id":966},[14,9048,970,9049,220,9051,220,9053,980],{},[77,9050,973],{},[77,9052,976],{},[77,9054,979],{},[10,9056,984],{"id":983},[14,9058,987],{},[135,9060,9062],{"className":9061,"code":991,"language":140,"meta":141},[138],[77,9063,991],{"__ignoreMap":141},[14,9065,996,9066,999],{},[77,9067,373],{},[47,9069,9070],{},[14,9071,1004],{},[10,9073,1007],{"id":1007},[21,9075,9076,9081,9086,9091,9096],{},[24,9077,9078],{},[58,9079,1017],{"href":1014,"rel":9080},[1016],[24,9082,9083],{},[58,9084,1024],{"href":1022,"rel":9085},[1016],[24,9087,9088],{},[58,9089,1031],{"href":1029,"rel":9090},[1016],[24,9092,9093],{},[58,9094,1038],{"href":1036,"rel":9095},[1016],[24,9097,9098],{},[58,9099,1045],{"href":1043,"rel":9100},[1016],[1047,9102,1049],{},{"title":141,"searchDepth":504,"depth":504,"links":9104},[9105,9108,9109,9110,9111,9112,9113,9114,9115,9116,9117,9118,9119,9120,9121,9122,9123,9124,9131,9132],{"id":12,"depth":504,"text":12,"children":9106},[9107],{"id":45,"depth":510,"text":45},{"id":54,"depth":504,"text":54},{"id":71,"depth":504,"text":72},{"id":123,"depth":504,"text":124},{"id":173,"depth":504,"text":174},{"id":227,"depth":504,"text":228},{"id":348,"depth":504,"text":349},{"id":382,"depth":504,"text":383},{"id":425,"depth":504,"text":426},{"id":482,"depth":504,"text":483},{"id":593,"depth":504,"text":594},{"id":619,"depth":504,"text":620},{"id":696,"depth":504,"text":697},{"id":749,"depth":504,"text":750},{"id":846,"depth":504,"text":847},{"id":868,"depth":504,"text":869},{"id":895,"depth":504,"text":896},{"id":934,"depth":504,"text":935,"children":9125},[9126,9127,9128,9129,9130],{"id":938,"depth":510,"text":939},{"id":945,"depth":510,"text":946},{"id":952,"depth":510,"text":953},{"id":959,"depth":510,"text":960},{"id":966,"depth":510,"text":967},{"id":983,"depth":504,"text":984},{"id":1007,"depth":504,"text":1007},{},{"title":5,"description":1081},1784931653043]