[{"data":1,"prerenderedAt":14165},["ShallowReactive",2],{"java:rocketmq-message-types-and-use-cases\u002F":3,"java-wiki-navigation":1219},{"id":4,"title":5,"body":6,"commentId":1205,"description":1206,"difficulty":1207,"draft":1208,"extension":1209,"meta":1210,"navigation":1211,"order":1212,"path":1213,"section":1214,"seo":1215,"stem":1216,"updated":1217,"__hash__":1218},"java\u002Fjava\u002Frocketmq-message-types-and-use-cases.md","RocketMQ 都有哪些消息类型？使用场景是什么？",{"type":7,"value":8,"toc":1149},"minimark",[9,13,17,21,50,53,56,64,67,81,84,88,91,167,170,173,188,191,195,200,203,213,216,219,224,228,231,254,257,263,266,270,273,284,287,291,295,298,301,307,310,316,327,331,334,337,343,346,350,353,357,360,363,366,369,372,378,382,402,406,409,412,429,432,436,440,443,449,452,473,477,483,486,490,513,516,522,525,529,536,553,556,560,563,569,576,579,583,587,590,595,598,606,609,615,619,625,628,634,637,640,657,661,664,667,673,676,693,696,700,703,709,712,772,775,779,783,786,792,795,809,813,816,819,825,831,844,847,851,854,857,874,878,881,887,900,904,907,913,916,920,923,929,932,967,970,973,992,995,1009,1012,1016,1020,1023,1027,1030,1034,1037,1041,1044,1048,1051,1055,1059,1062,1066,1069,1073,1076,1080,1083,1087,1090,1093,1145],[10,11,5],"h1",{"id":12},"rocketmq-都有哪些消息类型使用场景是什么",[14,15,16],"h2",{"id":16},"面试回答",[18,19,20],"p",{},"RocketMQ 5.x 按消息的传输语义正式分为四种类型：",[22,23,24,32,38,44],"ol",{},[25,26,27,31],"li",{},[28,29,30],"strong",{},"普通消息 Normal","：没有顺序、定时或事务等特殊语义，发送成功后即可被消费，适合微服务解耦、异步通知、日志和数据集成。",[25,33,34,37],{},[28,35,36],{},"顺序消息 FIFO","：同一 Message Group 内的消息按照发送、存储和投递顺序处理，适合同一订单的创建、支付、发货等状态流转，以及 Binlog 增量同步。",[25,39,40,43],{},[28,41,42],{},"定时与延时消息 Delay","：消息到指定时间后才对消费者可见，适合订单超时关闭、延迟检查、定时通知和任务调度。",[25,45,46,49],{},[28,47,48],{},"事务消息 Transaction","：通过半事务消息、本地事务确认和事务回查，保证本地事务与消息发送的最终一致性，适合下单后发放积分、扣款后发送通知等场景。",[18,51,52],{},"RocketMQ 5.x 可以为 Topic 声明消息类型，一个 Topic 只应承载一种消息类型。批量消息属于发送方式，重试消息和死信消息属于消费失败后的系统处理状态，Request-Reply 属于通信模式，它们都不属于上述四种正式 MessageType。",[18,54,55],{},"一句话总结：",[57,58,59],"blockquote",{},[18,60,61],{},[28,62,63],{},"无特殊要求用普通消息；同一业务实体需要按序处理用 FIFO；需要未来触发用 Delay；本地事务必须和消息发送保持最终一致用 Transaction。",[14,65,66],{"id":66},"一张图看懂",[18,68,69],{},[70,71,77],"a",{"href":72,"target":73,"rel":74},"\u002Fimages\u002Fwiki\u002Fjava\u002Frocketmq-message-types-and-use-cases.svg","_blank",[75,76],"noopener","noreferrer",[78,79],"img",{"src":72,"alt":80},"RocketMQ 四种消息类型、核心语义、典型场景及选择方法",[14,82,83],{"id":83},"详细讲解",[14,85,87],{"id":86},"一先明确-rocketmq-的正式分类","一、先明确 RocketMQ 的正式分类",[18,89,90],{},"RocketMQ 5.x 定义了四种 MessageType：",[92,93,94,110],"table",{},[95,96,97],"thead",{},[98,99,100,104,107],"tr",{},[101,102,103],"th",{},"消息类型",[101,105,106],{},"核心语义",[101,108,109],{},"典型场景",[111,112,113,128,141,154],"tbody",{},[98,114,115,122,125],{},[116,117,118],"td",{},[119,120,121],"code",{},"NORMAL",[116,123,124],{},"发送后立即进入可消费状态",[116,126,127],{},"异步解耦、事件通知、日志传输",[98,129,130,135,138],{},[116,131,132],{},[119,133,134],{},"FIFO",[116,136,137],{},"同一 Message Group 内按顺序处理",[116,139,140],{},"订单状态流转、Binlog 同步",[98,142,143,148,151],{},[116,144,145],{},[119,146,147],{},"DELAY",[116,149,150],{},"到指定时间后才允许消费",[116,152,153],{},"超时关闭、延迟检查、定时任务",[98,155,156,161,164],{},[116,157,158],{},[119,159,160],{},"TRANSACTION",[116,162,163],{},"本地事务与消息发送最终一致",[116,165,166],{},"交易完成后驱动下游业务",[18,168,169],{},"这四种类型不是按照消息内容划分，而是按照消息需要具备的传输和投递语义划分。",[18,171,172],{},"例如，同样是“订单创建”事件：",[174,175,176,179,182,185],"ul",{},[25,177,178],{},"创建后立即异步通知风控，可以使用普通消息；",[25,180,181],{},"必须与后续支付、发货事件保持同一订单内的顺序，可以使用顺序消息；",[25,183,184],{},"创建三十分钟后检查是否支付，可以使用延时消息；",[25,186,187],{},"只有订单数据库事务成功后才能向下游投递，可以使用事务消息。",[18,189,190],{},"因此，消息类型的选择取决于业务语义，而不是消息中装了什么数据。",[14,192,194],{"id":193},"二普通消息-normal","二、普通消息 Normal",[196,197,199],"h3",{"id":198},"_1-什么是普通消息","1. 什么是普通消息",[18,201,202],{},"普通消息没有顺序、定时和事务等特殊语义。Broker 接收并保存消息后，消息进入 Ready 状态，Consumer 可以正常获取并处理。",[204,205,211],"pre",{"className":206,"code":208,"language":209,"meta":210},[207],"language-text","Producer\n   │ 发送\n   ▼\nBroker 持久化\n   │ 立即可见\n   ▼\nConsumer 消费\n","text","",[119,212,208],{"__ignoreMap":210},[18,214,215],{},"“普通”不代表不可靠。普通消息同样可以使用发送确认、Broker 持久化、消费重试、死信队列和业务幂等机制。",[18,217,218],{},"它只表示：",[57,220,221],{},[18,222,223],{},"消息之间没有需要 RocketMQ 额外维护的顺序、时间或事务关系。",[196,225,227],{"id":226},"_2-适用场景","2. 适用场景",[18,229,230],{},"普通消息适合绝大多数异步业务：",[174,232,233,236,239,242,245,248,251],{},[25,234,235],{},"微服务之间异步解耦；",[25,237,238],{},"用户注册后异步发送欢迎短信；",[25,240,241],{},"订单创建后异步更新搜索索引；",[25,243,244],{},"操作日志、埋点和审计事件传输；",[25,246,247],{},"缓存失效通知；",[25,249,250],{},"数据采集和数据集成；",[25,252,253],{},"不要求严格顺序的业务事件广播。",[18,255,256],{},"例如，订单创建后需要通知多个独立系统：",[204,258,261],{"className":259,"code":260,"language":209,"meta":210},[207],"订单服务\n   │\n   └── OrderCreated 普通消息\n           ├── 风控服务\n           ├── 推荐服务\n           └── 数据分析服务\n",[119,262,260],{"__ignoreMap":210},[18,264,265],{},"这些下游之间不存在必须先后执行的关系，使用普通消息能够获得更好的并发能力。",[196,267,269],{"id":268},"_3-不适合的场景","3. 不适合的场景",[18,271,272],{},"以下要求不能只依赖普通消息：",[174,274,275,278,281],{},[25,276,277],{},"同一订单的支付必须在创建之后处理；",[25,279,280],{},"消息必须在未来某个时间触发；",[25,282,283],{},"本地数据库事务失败时消息绝对不能对下游可见。",[18,285,286],{},"这时应分别考虑 FIFO、Delay 或 Transaction。",[14,288,290],{"id":289},"三顺序消息-fifo","三、顺序消息 FIFO",[196,292,294],{"id":293},"_1-顺序消息保证什么","1. 顺序消息保证什么",[18,296,297],{},"RocketMQ 5.x 使用 Message Group 标识需要保持顺序的一组消息。同一个 Message Group 中的消息按照先进先出顺序处理。",[18,299,300],{},"例如以订单号作为 Message Group：",[204,302,305],{"className":303,"code":304,"language":209,"meta":210},[207],"Message Group = order-1001\n\n创建订单 → 支付成功 → 仓库出库 → 物流发货\n",[119,306,304],{"__ignoreMap":210},[18,308,309],{},"另一个订单可以使用不同的 Message Group：",[204,311,314],{"className":312,"code":313,"language":209,"meta":210},[207],"order-1001：创建 → 支付 → 发货\norder-1002：创建 → 取消\n",[119,315,313],{"__ignoreMap":210},[18,317,318,319,322,323,326],{},"两个订单之间不要求顺序，可以并发处理；同一订单内部保持顺序。这通常称为",[28,320,321],{},"局部顺序","或",[28,324,325],{},"分组顺序","。",[196,328,330],{"id":329},"_2-为什么不追求整个-topic-的全局顺序","2. 为什么不追求整个 Topic 的全局顺序",[18,332,333],{},"如果一个 Topic 的全部消息都要求全局顺序，就只能把所有消息串行发送、存储和消费，并发能力会受到严重限制。",[18,335,336],{},"按订单号、用户 ID、账户 ID 等业务键划分 Message Group，可以做到：",[204,338,341],{"className":339,"code":340,"language":209,"meta":210},[207],"组内有序\n+\n组间并行\n",[119,342,340],{"__ignoreMap":210},[18,344,345],{},"这更符合大多数业务的真实要求。",[196,347,349],{"id":348},"_3-保证顺序需要满足哪些条件","3. 保证顺序需要满足哪些条件",[18,351,352],{},"完整的顺序需要覆盖三个阶段。",[354,355,356],"h4",{"id":356},"发送顺序",[18,358,359],{},"同一 Message Group 的消息需要由生产端按业务顺序串行发送。如果多个线程或多个生产者同时发送，即使 Message Group 相同，RocketMQ 也无法推断业务上谁应该在前。",[354,361,362],{"id":362},"存储顺序",[18,364,365],{},"RocketMQ 将同一 Message Group 的消息存入同一个队列，并保持接收顺序。",[354,367,368],{"id":368},"消费顺序",[18,370,371],{},"Consumer 必须遵循“接收、处理、确认”的顺序。不能收到消息后随意提交给异步线程并立即确认，否则业务实际完成顺序仍可能被打乱。",[204,373,376],{"className":374,"code":375,"language":209,"meta":210},[207],"发送有序\n   ↓\n存储有序\n   ↓\n投递有序\n   ↓\n业务处理有序\n",[119,377,375],{"__ignoreMap":210},[196,379,381],{"id":380},"_4-适用场景","4. 适用场景",[174,383,384,387,390,393,396,399],{},[25,385,386],{},"同一订单的创建、支付、取消和发货；",[25,388,389],{},"同一账户的充值、扣款和退款；",[25,391,392],{},"同一用户的状态变更；",[25,394,395],{},"数据库 Binlog 增量同步；",[25,397,398],{},"工作流节点推进；",[25,400,401],{},"交易撮合等要求按到达顺序处理的场景。",[196,403,405],{"id":404},"_5-使用代价与风险","5. 使用代价与风险",[18,407,408],{},"顺序消息会牺牲部分并发能力，并且前面的消息处理失败时，后续消息可能等待。",[18,410,411],{},"需要重点关注：",[174,413,414,417,420,423,426],{},[25,415,416],{},"Message Group 是否过热；",[25,418,419],{},"单条消息是否持续重试并阻塞后续消息；",[25,421,422],{},"消费逻辑是否包含慢 SQL、慢 RPC；",[25,424,425],{},"是否错误地在消费端进行异步并发处理；",[25,427,428],{},"是否真的需要顺序，还是使用状态机和幂等就能解决。",[18,430,431],{},"不要把所有消息都设置为同一个 Message Group，否则会把系统退化成串行消费。",[14,433,435],{"id":434},"四定时与延时消息-delay","四、定时与延时消息 Delay",[196,437,439],{"id":438},"_1-定时消息和延时消息有什么区别","1. 定时消息和延时消息有什么区别",[18,441,442],{},"业务表达方式不同，但在 RocketMQ 5.x 中本质相同：都是设置一个未来的投递时间戳。",[204,444,447],{"className":445,"code":446,"language":209,"meta":210},[207],"延时消息：10 分钟后投递\n定时消息：2026-07-26 18:00:00 投递\n",[119,448,446],{"__ignoreMap":210},[18,450,451],{},"延时消息也需要先计算出最终投递时间：",[204,453,457],{"className":454,"code":455,"language":456,"meta":210,"style":210},"language-java shiki shiki-themes github-light github-dark","long deliveryTimestamp =\n        System.currentTimeMillis() + Duration.ofMinutes(10).toMillis();\n","java",[119,458,459,467],{"__ignoreMap":210},[460,461,464],"span",{"class":462,"line":463},"line",1,[460,465,466],{},"long deliveryTimestamp =\n",[460,468,470],{"class":462,"line":469},2,[460,471,472],{},"        System.currentTimeMillis() + Duration.ofMinutes(10).toMillis();\n",[196,474,476],{"id":475},"_2-消息生命周期","2. 消息生命周期",[204,478,481],{"className":479,"code":480,"language":209,"meta":210},[207],"Producer 发送\n      ↓\nTiming：Broker 按时间暂存\n      ↓ 到达投递时间\nReady：转入普通存储并对 Consumer 可见\n      ↓\nConsumer 消费并确认\n",[119,482,480],{"__ignoreMap":210},[18,484,485],{},"消息到达 Broker 并不代表 Consumer 立即可见。只有达到投递时间，消息才进入可以消费的状态。",[196,487,489],{"id":488},"_3-适用场景","3. 适用场景",[174,491,492,495,498,501,504,507,510],{},[25,493,494],{},"订单创建三十分钟后检查是否支付；",[25,496,497],{},"支付处理中，五分钟后查询渠道结果；",[25,499,500],{},"优惠券到期提醒；",[25,502,503],{},"用户预约通知；",[25,505,506],{},"延迟重试；",[25,508,509],{},"定时清理任务；",[25,511,512],{},"分布式定时调度。",[18,514,515],{},"例如订单超时关闭：",[204,517,520],{"className":518,"code":519,"language":209,"meta":210},[207],"创建订单\n   │\n   ├── 写入订单数据\n   └── 发送 30 分钟后投递的 Delay 消息\n                         ↓\n                 Consumer 查询订单状态\n                    ├── 未支付：关闭订单\n                    └── 已支付：忽略消息\n",[119,521,519],{"__ignoreMap":210},[18,523,524],{},"Consumer 收到延时消息后仍然必须重新检查业务状态。消息表达的是“现在应该检查”，不是“订单一定未支付”。",[196,526,528],{"id":527},"_4-延时消息不是精确定时器","4. 延时消息不是精确定时器",[18,530,531,532,535],{},"设置的时间表示消息",[28,533,534],{},"不应早于该时间被消费","，但以下因素可能使实际处理晚于目标时间：",[174,537,538,541,544,547,550],{},[25,539,540],{},"Broker 当前负载较高；",[25,542,543],{},"大量消息设置了相同投递时间；",[25,545,546],{},"消费端发生积压；",[25,548,549],{},"Consumer 执行较慢或暂时不可用；",[25,551,552],{},"存储系统故障或重启。",[18,554,555],{},"因此，对秒级严格准点有强要求的业务不能只依赖消息到达时间，还要监控投递延迟并准备补偿扫描。",[196,557,559],{"id":558},"_5-rocketmq-4x-与-5x-的区别","5. RocketMQ 4.x 与 5.x 的区别",[18,561,562],{},"RocketMQ 4.x 常见实现使用固定的延迟级别，例如：",[204,564,567],{"className":565,"code":566,"language":209,"meta":210},[207],"1 秒、5 秒、10 秒、30 秒、1 分钟……\n",[119,568,566],{"__ignoreMap":210},[18,570,571,572,575],{},"生产者通过 ",[119,573,574],{},"delayTimeLevel"," 选择预设级别。",[18,577,578],{},"RocketMQ 5.x 的正式 Delay 消息使用毫秒级 Unix 时间戳表示投递时间，不再局限于旧版固定延迟级别。面试时需要先说明讨论的是哪个版本，避免把 4.x 的十八个延迟级别直接当作 5.x 的唯一实现。",[14,580,582],{"id":581},"五事务消息-transaction","五、事务消息 Transaction",[196,584,586],{"id":585},"_1-事务消息解决什么问题","1. 事务消息解决什么问题",[18,588,589],{},"事务消息解决的是：",[57,591,592],{},[18,593,594],{},"本地事务与消息发送之间的最终一致性。",[18,596,597],{},"例如订单服务需要同时完成：",[22,599,600,603],{},[25,601,602],{},"在本地数据库创建订单；",[25,604,605],{},"向 RocketMQ 发送订单创建消息。",[18,607,608],{},"如果先提交数据库再发送消息，数据库成功后消息可能发送失败；如果先发送消息再写数据库，消息可能已被消费，但数据库事务最终失败。",[204,610,613],{"className":611,"code":612,"language":209,"meta":210},[207],"数据库事务成功，消息失败  → 下游永远不知道订单已创建\n消息发送成功，数据库失败  → 下游处理了一笔不存在的订单\n",[119,614,612],{"__ignoreMap":210},[196,616,618],{"id":617},"_2-核心流程","2. 核心流程",[204,620,623],{"className":621,"code":622,"language":209,"meta":210},[207],"Producer 发送 Half Message\n           ↓\nBroker 保存消息，但暂不向 Consumer 投递\n           ↓\nProducer 执行本地事务\n       ┌───┴────────┐\n       │            │\n    Commit       Rollback\n       │            │\n消息可见并投递    消息回滚\n",[119,624,622],{"__ignoreMap":210},[18,626,627],{},"如果 Broker 长时间没有收到明确结果，会回查 Producer：",[204,629,632],{"className":630,"code":631,"language":209,"meta":210},[207],"Broker 发起事务回查\n          ↓\nProducer 查询本地事务记录\n     ┌────┼─────┐\n  Commit Unknown Rollback\n",[119,633,631],{"__ignoreMap":210},[18,635,636],{},"Producer 的事务回查不能只依赖内存变量，需要根据订单表、事务日志或本地 Outbox 等持久化数据判断。",[196,638,489],{"id":639},"_3-适用场景-1",[174,641,642,645,648,651,654],{},[25,643,644],{},"订单创建成功后通知库存、积分系统；",[25,646,647],{},"扣款成功后发送账务事件；",[25,649,650],{},"账户入账成功后通知下游；",[25,652,653],{},"数据库状态变化后更新搜索索引；",[25,655,656],{},"本地事务成功后驱动跨服务异步流程。",[196,658,660],{"id":659},"_4-事务消息不保证什么","4. 事务消息不保证什么",[18,662,663],{},"事务消息保证的是本地事务与消息是否投递之间的最终一致性，不等于整个分布式业务已经成功。",[18,665,666],{},"例如：",[204,668,671],{"className":669,"code":670,"language":209,"meta":210},[207],"订单本地事务成功\n       ↓\n事务消息 Commit\n       ↓\n积分服务消费失败\n",[119,672,670],{"__ignoreMap":210},[18,674,675],{},"RocketMQ 可以重试投递，但积分服务仍需实现：",[174,677,678,681,684,687,690],{},[25,679,680],{},"消费幂等；",[25,682,683],{},"失败重试；",[25,685,686],{},"死信处理；",[25,688,689],{},"对账与补偿；",[25,691,692],{},"业务状态机。",[18,694,695],{},"事务消息也不是跨多个数据库的强一致分布式事务。它更适合能够接受异步执行和最终一致性的业务。",[14,697,699],{"id":698},"六四种消息类型如何选择","六、四种消息类型如何选择",[18,701,702],{},"可以按照下面的顺序判断：",[204,704,707],{"className":705,"code":706,"language":209,"meta":210},[207],"本地事务是否必须与消息发送保持最终一致？\n    ├── 是 → Transaction\n    └── 否\n         ↓\n消息是否需要到未来某个时间才可消费？\n    ├── 是 → Delay\n    └── 否\n         ↓\n同一业务实体的事件是否必须按顺序处理？\n    ├── 是 → FIFO\n    └── 否 → Normal\n",[119,708,706],{"__ignoreMap":210},[18,710,711],{},"对应关系：",[92,713,714,727],{},[95,715,716],{},[98,717,718,721,724],{},[101,719,720],{},"业务要求",[101,722,723],{},"推荐类型",[101,725,726],{},"关键设计点",[111,728,729,740,750,761],{},[98,730,731,734,737],{},[116,732,733],{},"只需要可靠异步传输",[116,735,736],{},"Normal",[116,738,739],{},"重试、幂等、监控",[98,741,742,745,747],{},[116,743,744],{},"同一订单内有先后关系",[116,746,134],{},[116,748,749],{},"Message Group、串行处理",[98,751,752,755,758],{},[116,753,754],{},"未来某个时间触发",[116,756,757],{},"Delay",[116,759,760],{},"投递时间、状态复查、延迟监控",[98,762,763,766,769],{},[116,764,765],{},"本地事务成功后才能投递",[116,767,768],{},"Transaction",[116,770,771],{},"Half Message、事务回查、持久化事务状态",[18,773,774],{},"不要因为高级消息类型“功能更多”就默认使用。每种特殊语义都会增加约束、运维成本或吞吐损耗；没有特殊要求时，普通消息通常是更简单的选择。",[14,776,778],{"id":777},"七这些算不算消息类型","七、这些算不算消息类型",[196,780,782],{"id":781},"_1-批量消息","1. 批量消息",[18,784,785],{},"批量消息不是独立 MessageType，而是一种发送或消费方式。",[204,787,790],{"className":788,"code":789,"language":209,"meta":210},[207],"单条发送：一次请求发送一条消息\n批量发送：一次请求携带多条消息\n",[119,791,789],{"__ignoreMap":210},[18,793,794],{},"批量能够减少网络往返和请求开销，适合日志采集、数据同步等吞吐优先的场景，但需要注意：",[174,796,797,800,803,806],{},[25,798,799],{},"控制整个批次的大小；",[25,801,802],{},"批次失败后的重试粒度；",[25,804,805],{},"顺序消息不应随意并行批处理；",[25,807,808],{},"批量消费部分成功时的幂等和重试问题。",[196,810,812],{"id":811},"_2-重试消息","2. 重试消息",[18,814,815],{},"重试消息是普通消息、FIFO 消息等消费失败后，RocketMQ 根据消费重试策略重新投递形成的系统处理过程，不是业务创建 Topic 时选择的正式 MessageType。",[18,817,818],{},"对于 PushConsumer，一次消费及失败重试可以简化为：",[204,820,823],{"className":821,"code":822,"language":209,"meta":210},[207],"Ready\n  ↓ Consumer 获取消息\nInflight\n  ├─ 消费成功并返回结果 → Commit\n  └─ 消费失败或等待结果超时 → WaitingRetry\n                                  ├─ 到达重试时间 → Ready\n                                  └─ 超过最大重试次数 → DLQ\n",[119,824,822],{"__ignoreMap":210},[18,826,827,830],{},[119,828,829],{},"Inflight"," 表示消息已经被 Consumer 获取，Consumer 正在执行业务逻辑，但消费结果还没有返回给 Broker。它可以理解为“正在处理中、等待确认”，不是一种新的消息类型。",[18,832,833,836,837,839,840,843],{},[119,834,835],{},"WaitingRetry"," 是 PushConsumer 重试状态机中的状态。消息消费失败或 Broker 等待消费结果超时后，如果尚未达到最大重试次数，消息会先进入 ",[119,838,835],{},"；重试间隔结束后重新回到 ",[119,841,842],{},"Ready","，等待下一次投递。",[18,845,846],{},"Consumer 必须按消息可能重复投递来设计幂等。",[196,848,850],{"id":849},"_3-死信消息","3. 死信消息",[18,852,853],{},"消息超过最大消费重试次数后，可能进入死信队列。死信消息代表消息进入异常隔离状态，也不是四种正式业务 MessageType 之一。",[18,855,856],{},"死信队列需要配套：",[174,858,859,862,865,868,871],{},[25,860,861],{},"告警；",[25,863,864],{},"失败原因记录；",[25,866,867],{},"人工检查；",[25,869,870],{},"修复后的重投；",[25,872,873],{},"对账和业务补偿。",[196,875,877],{"id":876},"_4-request-reply-消息","4. Request-Reply 消息",[18,879,880],{},"Request-Reply 是一种通信模式：",[204,882,885],{"className":883,"code":884,"language":209,"meta":210},[207],"请求方发送消息\n      ↓\n处理方消费并返回响应\n      ↓\n请求方等待响应\n",[119,886,884],{"__ignoreMap":210},[18,888,889,890,892,893,892,895,892,897,899],{},"它描述生产者和消费者如何交互，不属于 ",[119,891,121],{},"、",[119,894,134],{},[119,896,147],{},[119,898,160],{}," 之外的第五种正式 MessageType。",[196,901,903],{"id":902},"_5-同步异步和单向消息","5. 同步、异步和单向消息",[18,905,906],{},"同步发送、异步发送和单向发送描述的是 Producer 调用发送接口的方式，也不是消息类型。",[204,908,911],{"className":909,"code":910,"language":209,"meta":210},[207],"同步发送：等待 Broker 返回结果\n异步发送：通过回调接收结果\n单向发送：发送后不等待结果\n",[119,912,910],{"__ignoreMap":210},[18,914,915],{},"不要把“发送方式”和“消息传输语义”混为一谈。",[14,917,919],{"id":918},"八rocketmq-5x-为什么要求-topic-类型一致","八、RocketMQ 5.x 为什么要求 Topic 类型一致",[18,921,922],{},"RocketMQ 5.x 支持在 Topic 上声明消息类型：",[204,924,927],{"className":925,"code":926,"language":209,"meta":210},[207],".\u002Fbin\u002Fmqadmin updateTopic \\\n  -n 127.0.0.1:9876 \\\n  -t order-events \\\n  -c DefaultCluster \\\n  -a \"+message.type=FIFO\"\n",[119,928,926],{"__ignoreMap":210},[18,930,931],{},"其中：",[174,933,934,940,946,952],{},[25,935,936,939],{},[119,937,938],{},"-n"," 指定 NameServer 地址；",[25,941,942,945],{},[119,943,944],{},"-t"," 指定 Topic 名称；",[25,947,948,951],{},[119,949,950],{},"-c"," 指定目标集群；",[25,953,954,957,958,892,960,892,962,964,965,326],{},[119,955,956],{},"message.type"," 可设置为 ",[119,959,121],{},[119,961,134],{},[119,963,147],{}," 或 ",[119,966,160],{},[18,968,969],{},"上面创建的是 FIFO Topic。创建其他类型时，只需要替换最后一个参数的类型值。",[18,971,972],{},"启用类型校验后，一个 Topic 只允许发送与其类型一致的消息。例如：",[174,974,975,983,989],{},[25,976,977,979,980,982],{},[119,978,134],{}," 消息不能发送到 ",[119,981,121],{}," Topic；",[25,984,985,979,987,982],{},[119,986,160],{},[119,988,147],{},[25,990,991],{},"同一个 Topic 不应同时承载普通消息和顺序消息。",[18,993,994],{},"这样做的价值是：",[174,996,997,1000,1003,1006],{},[25,998,999],{},"让 Topic 的行为语义清晰；",[25,1001,1002],{},"避免客户端误用高级消息能力；",[25,1004,1005],{},"便于 Broker 按消息类型管理；",[25,1007,1008],{},"降低运维和故障排查复杂度。",[18,1010,1011],{},"为了兼容旧客户端，5.x 的强制类型校验能力需要结合服务端配置判断。面试中可以说“5.x 支持并推荐一个 Topic 对应一种消息类型”，不要绝对化为所有兼容部署都默认强制拒绝。",[14,1013,1015],{"id":1014},"九常见追问","九、常见追问",[196,1017,1019],{"id":1018},"_1-普通消息一定无序吗","1. 普通消息一定无序吗",[18,1021,1022],{},"普通消息在某个物理队列中自然存在存储先后，但业务没有建立端到端 FIFO 约束。多队列发送、并发消费、失败重试都可能使业务完成顺序变化，因此不能依赖普通消息保证业务顺序。",[196,1024,1026],{"id":1025},"_2-顺序消息能否保证整个-topic-全局有序","2. 顺序消息能否保证整个 Topic 全局有序",[18,1028,1029],{},"实际通常保证同一 Message Group 内有序。理论上把所有消息放入一个组可以接近全局串行，但会形成热点并严重限制吞吐，不建议这样设计。",[196,1031,1033],{"id":1032},"_3-延时消息到点后一定立即执行吗","3. 延时消息到点后一定立即执行吗",[18,1035,1036],{},"不一定。到点表示消息可以进入可消费状态，Broker 调度延迟、消息积压和 Consumer 处理能力都会影响最终业务执行时间。",[196,1038,1040],{"id":1039},"_4-事务消息消费成功是否与本地事务强一致","4. 事务消息消费成功是否与本地事务强一致",[18,1042,1043],{},"不是。事务消息只保证本地事务结果与消息是否投递之间的最终一致性。Consumer 的业务处理仍然是独立的异步过程，需要重试、幂等和补偿。",[196,1045,1047],{"id":1046},"_5-fifo-和-transaction-能否同时使用","5. FIFO 和 Transaction 能否同时使用",[18,1049,1050],{},"从 RocketMQ 5.x 的 Topic 类型模型看，一个 Topic 只对应一种正式消息类型，不能把同一条消息同时声明为 FIFO 和 Transaction。业务同时需要“事务一致性”和“顺序处理”时，需要重新审视领域流程，常见做法是用事务消息可靠发布业务事件，再由下游状态机、版本号、业务序号或独立 FIFO 流程处理顺序要求。",[14,1052,1054],{"id":1053},"十容易答错的地方","十、容易答错的地方",[196,1056,1058],{"id":1057},"错误一rocketmq-有五种消息包含批量消息","错误一：RocketMQ 有五种消息，包含批量消息",[18,1060,1061],{},"批量是发送方式，不是 RocketMQ 5.x 的正式 MessageType。正式类型是 Normal、FIFO、Delay、Transaction。",[196,1063,1065],{"id":1064},"错误二顺序消息保证整个-topic-全局有序","错误二：顺序消息保证整个 Topic 全局有序",[18,1067,1068],{},"通常保证同一 Message Group 内的局部顺序，不同组之间允许并发。",[196,1070,1072],{"id":1071},"错误三延时消息到了时间就保证业务已经执行","错误三：延时消息到了时间就保证业务已经执行",[18,1074,1075],{},"到达投递时间只表示消息对 Consumer 可见，实际业务完成还受积压、消费能力和重试影响。",[196,1077,1079],{"id":1078},"错误四事务消息保证上下游所有业务一起提交","错误四：事务消息保证上下游所有业务一起提交",[18,1081,1082],{},"事务消息保证本地事务与消息投递最终一致，下游消费结果仍然需要独立保障。",[196,1084,1086],{"id":1085},"错误五普通消息可靠性低","错误五：普通消息可靠性低",[18,1088,1089],{},"普通消息只是没有高级传输语义，不代表不持久化、不重试或不可靠。",[14,1091,1092],{"id":1092},"参考资料",[174,1094,1095,1103,1110,1117,1124,1131,1138],{},[25,1096,1097],{},[70,1098,1102],{"href":1099,"rel":1100},"https:\u002F\u002Frocketmq.apache.org\u002Fdocs\u002FdomainModel\u002F02topic\u002F",[1101],"nofollow","Apache RocketMQ 5.0：Topic 与 MessageType",[25,1104,1105],{},[70,1106,1109],{"href":1107,"rel":1108},"https:\u002F\u002Frocketmq.apache.org\u002Fdocs\u002FfeatureBehavior\u002F01normalmessage\u002F",[1101],"Apache RocketMQ 5.0：普通消息",[25,1111,1112],{},[70,1113,1116],{"href":1114,"rel":1115},"https:\u002F\u002Frocketmq.apache.org\u002Fdocs\u002FfeatureBehavior\u002F03fifomessage\u002F",[1101],"Apache RocketMQ 5.0：顺序消息",[25,1118,1119],{},[70,1120,1123],{"href":1121,"rel":1122},"https:\u002F\u002Frocketmq.apache.org\u002Fdocs\u002FfeatureBehavior\u002F02delaymessage\u002F",[1101],"Apache RocketMQ 5.0：定时与延时消息",[25,1125,1126],{},[70,1127,1130],{"href":1128,"rel":1129},"https:\u002F\u002Frocketmq.apache.org\u002Fdocs\u002FfeatureBehavior\u002F04transactionmessage\u002F",[1101],"Apache RocketMQ 5.0：事务消息",[25,1132,1133],{},[70,1134,1137],{"href":1135,"rel":1136},"https:\u002F\u002Frocketmq.apache.org\u002Fdocs\u002FfeatureBehavior\u002F10consumerretrypolicy\u002F",[1101],"Apache RocketMQ 5.0：消费重试",[25,1139,1140],{},[70,1141,1144],{"href":1142,"rel":1143},"https:\u002F\u002Frocketmq.apache.org\u002Fdocs\u002F4.x\u002Fproducer\u002F04message3\u002F",[1101],"Apache RocketMQ 4.x：延时消息级别",[1146,1147,1148],"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":210,"searchDepth":469,"depth":469,"links":1150},[1151,1152,1153,1154,1155,1161,1168,1175,1181,1182,1189,1190,1197,1204],{"id":16,"depth":469,"text":16},{"id":66,"depth":469,"text":66},{"id":83,"depth":469,"text":83},{"id":86,"depth":469,"text":87},{"id":193,"depth":469,"text":194,"children":1156},[1157,1159,1160],{"id":198,"depth":1158,"text":199},3,{"id":226,"depth":1158,"text":227},{"id":268,"depth":1158,"text":269},{"id":289,"depth":469,"text":290,"children":1162},[1163,1164,1165,1166,1167],{"id":293,"depth":1158,"text":294},{"id":329,"depth":1158,"text":330},{"id":348,"depth":1158,"text":349},{"id":380,"depth":1158,"text":381},{"id":404,"depth":1158,"text":405},{"id":434,"depth":469,"text":435,"children":1169},[1170,1171,1172,1173,1174],{"id":438,"depth":1158,"text":439},{"id":475,"depth":1158,"text":476},{"id":488,"depth":1158,"text":489},{"id":527,"depth":1158,"text":528},{"id":558,"depth":1158,"text":559},{"id":581,"depth":469,"text":582,"children":1176},[1177,1178,1179,1180],{"id":585,"depth":1158,"text":586},{"id":617,"depth":1158,"text":618},{"id":639,"depth":1158,"text":489},{"id":659,"depth":1158,"text":660},{"id":698,"depth":469,"text":699},{"id":777,"depth":469,"text":778,"children":1183},[1184,1185,1186,1187,1188],{"id":781,"depth":1158,"text":782},{"id":811,"depth":1158,"text":812},{"id":849,"depth":1158,"text":850},{"id":876,"depth":1158,"text":877},{"id":902,"depth":1158,"text":903},{"id":918,"depth":469,"text":919},{"id":1014,"depth":469,"text":1015,"children":1191},[1192,1193,1194,1195,1196],{"id":1018,"depth":1158,"text":1019},{"id":1025,"depth":1158,"text":1026},{"id":1032,"depth":1158,"text":1033},{"id":1039,"depth":1158,"text":1040},{"id":1046,"depth":1158,"text":1047},{"id":1053,"depth":469,"text":1054,"children":1198},[1199,1200,1201,1202,1203],{"id":1057,"depth":1158,"text":1058},{"id":1064,"depth":1158,"text":1065},{"id":1071,"depth":1158,"text":1072},{"id":1078,"depth":1158,"text":1079},{"id":1085,"depth":1158,"text":1086},{"id":1092,"depth":469,"text":1092},"wiki:java:rocketmq-message-types-and-use-cases","系统介绍 RocketMQ 5.x 的普通消息、顺序消息、定时与延时消息、事务消息，说明各自语义、适用场景、使用限制，并澄清批量消息、重试消息和死信消息是否属于消息类型。","intermediate",false,"md",{},true,78,"\u002Fjava\u002Frocketmq-message-types-and-use-cases","面试专题",{"title":5,"description":1206},"java\u002Frocketmq-message-types-and-use-cases","2026-07-26","zPNK0dDY9QBEyEm1LnCsD-ND3HLubjvBfZlwmFdve98",[1220,2645,3563,4751,5766,7110,8550,9594,13339],{"id":1221,"title":1222,"body":1223,"commentId":2634,"description":2635,"difficulty":2636,"draft":1208,"extension":1209,"meta":2637,"navigation":1211,"order":2638,"path":2639,"section":2640,"seo":2641,"stem":2642,"updated":2643,"__hash__":2644},"java\u002Fjava\u002Fjdk8-synchronized-lock-states.md","JDK 8 synchronized 设计与锁状态转换",{"type":7,"value":1224,"toc":2577},[1225,1229,1235,1246,1249,1255,1258,1260,1269,1274,1277,1282,1288,1292,1295,1301,1308,1311,1317,1320,1323,1371,1375,1378,1384,1390,1393,1402,1407,1411,1418,1433,1547,1553,1559,1562,1565,1571,1574,1578,1584,1587,1593,1600,1603,1636,1640,1643,1646,1649,1653,1656,1659,1666,1669,1674,1678,1681,1687,1690,1693,1696,1702,1705,1708,1711,1717,1720,1726,1732,1736,1743,1746,1752,1755,1766,1769,1772,1775,1781,1784,1787,1790,1794,1797,1800,1804,1807,1811,1814,1818,1821,1824,1829,1833,1836,1838,1841,1844,1850,1853,1856,1859,1863,1877,1880,1886,1889,1895,1898,1904,1907,1911,1914,1918,1921,1924,1927,1933,1939,1943,1946,1952,1955,1961,1964,1967,1971,1974,1988,1991,1994,2000,2003,2009,2013,2020,2026,2029,2035,2038,2041,2055,2058,2062,2067,2073,2079,2089,2094,2098,2101,2104,2113,2116,2119,2122,2126,2302,2306,2310,2316,2320,2323,2329,2332,2336,2342,2346,2349,2383,2386,2437,2440,2466,2469,2475,2478,2484,2488,2492,2495,2499,2502,2506,2509,2513,2516,2520,2523,2527,2530,2534,2564,2567,2575],[14,1226,1228],{"id":1227},"一先给出整体结论","一、先给出整体结论",[18,1230,1231,1234],{},[119,1232,1233],{},"synchronized"," 是 Java 语言提供的内置同步机制。它在语义上负责三件事：",[174,1236,1237,1240,1243],{},[25,1238,1239],{},"同一时刻只允许满足条件的线程进入临界区；",[25,1241,1242],{},"支持同一线程重复获取同一把锁，也就是可重入；",[25,1244,1245],{},"解锁 happens-before 后续对同一 Monitor 的加锁，保证相关共享数据的可见性和有序性。",[18,1247,1248],{},"在 JDK 8 HotSpot 中，为了避免所有同步操作都直接阻塞线程，JVM 会根据竞争情况使用不同实现：",[204,1250,1253],{"className":1251,"code":1252,"language":209,"meta":210},[207],"偏向锁：同一个线程反复进入，尽量避免原子操作\n轻量级锁：线程交替执行或短暂竞争，使用栈上 Lock Record 和 CAS\n重量级锁：竞争较强或必须使用 Monitor 能力时，通过 ObjectMonitor 管理等待线程\n",[119,1254,1252],{"__ignoreMap":210},[18,1256,1257],{},"通常所说的“无锁、偏向锁、轻量级锁、重量级锁”，是对对象头及其关联同步结构的概括，不是 Java 语言规范定义的四种锁类型。",[14,1259,66],{"id":66},[18,1261,1262],{},[70,1263,1266],{"href":1264,"target":73,"rel":1265},"\u002Fimages\u002Fwiki\u002Fjava\u002Fjdk8-synchronized-state-transitions.svg",[75,76],[78,1267],{"src":1264,"alt":1268},"JDK 8 synchronized 锁状态转换总览",[57,1270,1271],{},[18,1272,1273],{},"图中信息较多，可点击图片查看原图。",[18,1275,1276],{},"需要先纠正一个常见说法：",[57,1278,1279],{},[18,1280,1281],{},"“锁只能升级，不能降级”适合帮助理解竞争路径，但并不严谨。",[18,1283,1284,1285],{},"轻量级锁正常释放后会通过 CAS 恢复对象原来的 Mark Word；已经膨胀的 ObjectMonitor 也可能在后续安全点被 JVM 回收。更准确的说法是：",[28,1286,1287],{},"持锁竞争过程中通常不会为了当前请求立即从重量级锁退回轻量级锁，但对象状态并非终身只能单向变化。",[14,1289,1291],{"id":1290},"二synchronized-在字节码中的入口","二、synchronized 在字节码中的入口",[18,1293,1294],{},"同步代码块通常编译为：",[204,1296,1299],{"className":1297,"code":1298,"language":209,"meta":210},[207],"monitorenter\n   临界区代码\nmonitorexit\n",[119,1300,1298],{"__ignoreMap":210},[18,1302,1303,1304,1307],{},"编译器会为正常返回和异常退出路径生成对应的 ",[119,1305,1306],{},"monitorexit","，保证退出同步块时释放锁。",[18,1309,1310],{},"同步方法不会在方法体中出现这两个指令，而是在方法访问标志中使用：",[204,1312,1315],{"className":1313,"code":1314,"language":209,"meta":210},[207],"ACC_SYNCHRONIZED\n",[119,1316,1314],{"__ignoreMap":210},[18,1318,1319],{},"JVM 在调用和返回同步方法时完成 Monitor 的进入与退出。",[18,1321,1322],{},"无论入口是哪一种，最终都围绕同一个锁对象工作：",[92,1324,1325,1335],{},[95,1326,1327],{},[98,1328,1329,1332],{},[101,1330,1331],{},"写法",[101,1333,1334],{},"实际锁对象",[111,1336,1337,1348,1360],{},[98,1338,1339,1342],{},[116,1340,1341],{},"实例同步方法",[116,1343,1344,1345],{},"当前实例 ",[119,1346,1347],{},"this",[98,1349,1350,1353],{},[116,1351,1352],{},"静态同步方法",[116,1354,1355,1356,1359],{},"当前类对应的 ",[119,1357,1358],{},"Class"," 对象",[98,1361,1362,1365],{},[116,1363,1364],{},"同步代码块",[116,1366,1367,1370],{},[119,1368,1369],{},"synchronized (...)"," 中指定的对象",[14,1372,1374],{"id":1373},"三对象头与-mark-word","三、对象头与 Mark Word",[18,1376,1377],{},"普通 Java 对象的对象头主要包含：",[204,1379,1382],{"className":1380,"code":1381,"language":209,"meta":210},[207],"Mark Word\nKlass Pointer\n",[119,1383,1381],{"__ignoreMap":210},[18,1385,1386,1387,1389],{},"数组对象还会额外保存数组长度。",[119,1388,1233],{}," 的关键状态主要编码在 Mark Word 中。",[18,1391,1392],{},"以 JDK 8、64 位 HotSpot 为例，Mark Word 会复用同一段空间：",[18,1394,1395],{},[70,1396,1399],{"href":1397,"target":73,"rel":1398},"\u002Fimages\u002Fwiki\u002Fjava\u002Fjdk8-synchronized-mark-word.svg",[75,76],[78,1400],{"src":1397,"alt":1401},"JDK 8 HotSpot Mark Word 与同步结构",[57,1403,1404],{},[18,1405,1406],{},"可点击图片查看完整尺寸。",[196,1408,1410],{"id":1409},"统一按最低-3-位阅读","统一按最低 3 位阅读",[18,1412,1413,1414,1417],{},"为了避免一会儿看 2 位、一会儿看 3 位，本文后续统一展示 Mark Word 的最低 ",[28,1415,1416],{},"3 位","。但要先区分“展示宽度”和“字段含义”：",[174,1419,1420,1427,1430],{},[25,1421,1422,1423,1426],{},"真正的锁标志始终是最低 ",[28,1424,1425],{},"2 位","；",[25,1428,1429],{},"只有普通未锁定和偏向布局会把倒数第 3 位解释成“偏向位”；",[25,1431,1432],{},"轻量级锁、重量级锁保存的是对齐后的指针，本文把其最低 3 位也完整写出，但倒数第 3 位不再表示偏向。",[92,1434,1435,1451],{},[95,1436,1437],{},[98,1438,1439,1442,1445,1448],{},[101,1440,1441],{},"最低 3 位",[101,1443,1444],{},"最低 2 位锁标志",[101,1446,1447],{},"状态",[101,1449,1450],{},"倒数第 3 位如何解释",[111,1452,1453,1474,1493,1511,1529],{},[98,1454,1455,1460,1465,1468],{},[116,1456,1457],{},[119,1458,1459],{},"001",[116,1461,1462],{},[119,1463,1464],{},"01",[116,1466,1467],{},"普通未锁定",[116,1469,1470,1471],{},"偏向位为 ",[119,1472,1473],{},"0",[98,1475,1476,1481,1485,1488],{},[116,1477,1478],{},[119,1479,1480],{},"101",[116,1482,1483],{},[119,1484,1464],{},[116,1486,1487],{},"可偏向或已偏向",[116,1489,1470,1490],{},[119,1491,1492],{},"1",[98,1494,1495,1500,1505,1508],{},[116,1496,1497],{},[119,1498,1499],{},"000",[116,1501,1502],{},[119,1503,1504],{},"00",[116,1506,1507],{},"轻量级锁",[116,1509,1510],{},"对齐后的 Lock Record 指针低位，不是偏向位",[98,1512,1513,1518,1523,1526],{},[116,1514,1515],{},[119,1516,1517],{},"010",[116,1519,1520],{},[119,1521,1522],{},"10",[116,1524,1525],{},"重量级锁",[116,1527,1528],{},"对齐后的 ObjectMonitor 指针低位，不是偏向位",[98,1530,1531,1536,1541,1544],{},[116,1532,1533],{},[119,1534,1535],{},"011",[116,1537,1538],{},[119,1539,1540],{},"11",[116,1542,1543],{},"GC 标记",[116,1545,1546],{},"标记状态的一部分，不是偏向位",[18,1548,1549,1550,1552],{},"因此 ",[119,1551,1480],{}," 应当拆开读成：",[204,1554,1557],{"className":1555,"code":1556,"language":209,"meta":210},[207],"偏向位 1 ｜ 锁标志 01\n",[119,1558,1556],{"__ignoreMap":210},[18,1560,1561],{},"它不是一个三位的“锁标志”。本文使用三位只是为了让所有状态在图表中对齐、便于比较。",[18,1563,1564],{},"其中偏向状态可以继续区分：",[204,1566,1569],{"className":1567,"code":1568,"language":209,"meta":210},[207],"匿名偏向：线程指针为空，允许第一个线程认领\n已偏向：线程指针指向获得偏向的 JavaThread\n",[119,1570,1568],{"__ignoreMap":210},[18,1572,1573],{},"Mark Word 是一块被复用的空间。例如普通未锁定对象可以直接保存 identity hash；偏向对象需要保存线程指针，因此二者不能同时使用同一布局。",[196,1575,1577],{"id":1576},"epoch-到底是什么","epoch 到底是什么",[18,1579,1580,1583],{},[119,1581,1582],{},"epoch"," 可以理解为某个类的“偏向版本号”，它不是时间戳，也不是对象年龄。",[18,1585,1586],{},"偏向锁不只依赖对象自身。支持偏向的类会在类的 prototype header 中保存一个当前 epoch；每个偏向对象的 Mark Word 中也会记录对象认领时使用的 epoch。线程进入偏向对象时，会比较两者：",[204,1588,1591],{"className":1589,"code":1590,"language":209,"meta":210},[207],"对象 epoch == 类当前 epoch\n    → 这份偏向记录仍属于当前版本，继续检查偏向线程\n\n对象 epoch != 类当前 epoch\n    → 这份偏向记录已经过期，可以进入重偏向或撤销流程\n",[119,1592,1590],{"__ignoreMap":210},[18,1594,1595,1596,1599],{},"它主要服务于",[28,1597,1598],{},"批量重偏向","。当一批对象从线程 A 整体转交给线程 B 使用时，JVM 不必立即逐个改写所有对象头，而是先提升类的 epoch。旧对象之后被访问时，会因为 epoch 不匹配而按新版本重新处理。",[18,1601,1602],{},"所以需要把两个容易混淆的字段分开：",[92,1604,1605,1615],{},[95,1606,1607],{},[98,1608,1609,1612],{},[101,1610,1611],{},"字段",[101,1613,1614],{},"含义",[111,1616,1617,1627],{},[98,1618,1619,1624],{},[116,1620,1621],{},[119,1622,1623],{},"age",[116,1625,1626],{},"对象的 GC 年龄，用于分代回收",[98,1628,1629,1633],{},[116,1630,1631],{},[119,1632,1582],{},[116,1634,1635],{},"类的偏向版本，用于判断对象上的偏向记录是否过期",[14,1637,1639],{"id":1638},"四jdk-8-为什么需要三种锁实现","四、JDK 8 为什么需要三种锁实现",[18,1641,1642],{},"不同竞争场景适合不同成本的同步方式。",[196,1644,1645],{"id":1645},"只有一个线程反复进入",[18,1647,1648],{},"如果一个对象长期只被同一个线程加锁，每次都执行 CAS 仍然是额外成本。偏向锁把对象与线程关联起来，后续同一线程进入时主要检查对象头是否仍偏向自己。",[196,1650,1652],{"id":1651},"多线程交替执行但没有同时竞争","多线程交替执行，但没有同时竞争",[18,1654,1655],{},"例如线程 A 使用完后线程 B 才进入。此时没有必要挂起线程，可以通过栈上 Lock Record 和 CAS 完成加锁、解锁。",[196,1657,1658],{"id":1658},"多线程同时争抢或需要等待队列",[18,1660,1661,1662,1665],{},"当 CAS 快速路径无法解决竞争，或者调用 ",[119,1663,1664],{},"wait()"," 需要条件等待集合时，JVM 使用 ObjectMonitor 管理 Owner、竞争线程和等待线程。",[18,1667,1668],{},"所以这三种实现的目标不是简单比较“谁更高级”，而是：",[57,1670,1671],{},[18,1672,1673],{},"根据实际竞争强度，在原子操作、CPU 自旋、线程阻塞和唤醒成本之间做权衡。",[14,1675,1677],{"id":1676},"五对象初始状态普通未锁定还是匿名偏向","五、对象初始状态：普通未锁定还是匿名偏向",[18,1679,1680],{},"JDK 8 默认启用偏向锁，但默认存在启动延迟：",[204,1682,1685],{"className":1683,"code":1684,"language":209,"meta":210},[207],"-XX:+UseBiasedLocking\n-XX:BiasedLockingStartupDelay=4000\n",[119,1686,1684],{"__ignoreMap":210},[18,1688,1689],{},"因此需要区分对象创建时机：",[196,1691,1692],{"id":1692},"偏向锁尚未启用或被关闭",[18,1694,1695],{},"新对象通常使用普通未锁定布局：",[204,1697,1700],{"className":1698,"code":1699,"language":209,"meta":210},[207],"[identity hash | age | 0 | 01]\n",[119,1701,1699],{"__ignoreMap":210},[18,1703,1704],{},"线程第一次进入同步块时，直接尝试建立轻量级锁。",[196,1706,1707],{"id":1707},"偏向锁已经启用",[18,1709,1710],{},"支持偏向的类创建新对象时，对象通常处于匿名偏向状态：",[204,1712,1715],{"className":1713,"code":1714,"language":209,"meta":210},[207],"[0 | epoch | age | 1 | 01]\n",[119,1716,1714],{"__ignoreMap":210},[18,1718,1719],{},"因此严格来说，不应把这条路径描述为：",[204,1721,1724],{"className":1722,"code":1723,"language":209,"meta":210},[207],"新对象先是无锁，然后第一次进入才“升级”为可偏向状态\n",[119,1725,1723],{"__ignoreMap":210},[18,1727,1728,1729],{},"更准确的说法是：",[28,1730,1731],{},"对象创建时便根据类的 prototype header 获得普通未锁定或匿名偏向的 Mark Word。",[14,1733,1735],{"id":1734},"六偏向锁的获取与重入","六、偏向锁的获取与重入",[18,1737,1738,1739,1742],{},"线程进入一个匿名偏向对象时，会尝试通过 CAS 把自己的 ",[119,1740,1741],{},"JavaThread*","、类当前的 epoch 等信息写入 Mark Word。",[18,1744,1745],{},"成功后，对象变成：",[204,1747,1750],{"className":1748,"code":1749,"language":209,"meta":210},[207],"[当前 JavaThread* | epoch | age | 1 | 01]\n",[119,1751,1749],{"__ignoreMap":210},[18,1753,1754],{},"同一个线程以后再次进入时，主要检查：",[22,1756,1757,1760,1763],{},[25,1758,1759],{},"Mark Word 是否仍是偏向模式；",[25,1761,1762],{},"偏向线程是否是当前线程；",[25,1764,1765],{},"对象 epoch 是否仍与类的 prototype header 匹配。",[18,1767,1768],{},"全部满足时可直接进入，不需要再次通过 CAS 抢占锁。",[196,1770,1771],{"id":1771},"偏向锁退出为什么不清除线程信息",[18,1773,1774],{},"偏向锁针对的就是“同一个线程还会再次进入”这一场景。同步块结束后，Mark Word 通常仍保留对原线程的偏向：",[204,1776,1779],{"className":1777,"code":1778,"language":209,"meta":210},[207],"进入前：偏向线程 A\n执行中：偏向线程 A\n退出后：仍偏向线程 A\n",[119,1780,1778],{"__ignoreMap":210},[18,1782,1783],{},"它不是表示线程 A 永远占用临界区，而是表示下一次线程 A 再进入时可以走低成本路径。",[196,1785,1786],{"id":1786},"偏向锁如何实现可重入",[18,1788,1789],{},"对象头记录了偏向线程，但不通过一个公共计数器记录每次进入。JVM 可以结合当前线程栈上的锁记录识别同步嵌套。对同一偏向线程而言，重复进入不需要竞争性地修改对象头。",[14,1791,1793],{"id":1792},"七其他线程访问偏向对象时发生什么","七、其他线程访问偏向对象时发生什么",[18,1795,1796],{},"线程 B 遇到偏向线程 A 的对象时，不能简单地直接覆盖线程指针。JVM 需要先判断原偏向是否仍然有效。",[18,1798,1799],{},"可能出现以下路径。",[196,1801,1803],{"id":1802},"_1-epoch-已过期","1. epoch 已过期",[18,1805,1806],{},"类发生批量重偏向后，旧对象的 epoch 可能落后于类的 epoch。线程 B 可以尝试通过 CAS 将对象重新偏向自己。",[196,1808,1810],{"id":1809},"_2-原线程已经不再持有这把锁","2. 原线程已经不再持有这把锁",[18,1812,1813],{},"JVM 可以撤销原偏向，使对象恢复为普通未锁定状态，或者在允许的情况下重新偏向新线程。",[196,1815,1817],{"id":1816},"_3-原线程仍在同步块中","3. 原线程仍在同步块中",[18,1819,1820],{},"JVM 需要检查原线程栈，撤销偏向并重建合法的锁状态。如果此时存在实际竞争，后续会进入轻量级竞争或直接膨胀为 ObjectMonitor。",[18,1822,1823],{},"因此：",[57,1825,1826],{},[18,1827,1828],{},"另一个线程访问偏向对象，不等于必然直接升级为重量级锁。是否重偏向、撤销为普通状态、转换为轻量级锁或膨胀，需要结合 epoch、类级启发式统计和原线程是否仍持锁判断。",[14,1830,1832],{"id":1831},"八批量重偏向与批量撤销","八、批量重偏向与批量撤销",[18,1834,1835],{},"如果某个类的大量对象不断被不同线程使用，逐个在安全点撤销偏向的成本会很高。HotSpot 会对同一类的偏向撤销次数做启发式统计。",[196,1837,1598],{"id":1598},[18,1839,1840],{},"当撤销达到一定阈值时，JVM 可以提升类 prototype header 中的 epoch。旧对象不需要立刻逐个修改；之后线程发现对象 epoch 过期时，再尝试将它偏向当前线程。",[18,1842,1843],{},"适合这种模式：",[204,1845,1848],{"className":1846,"code":1847,"language":209,"meta":210},[207],"一批对象先由线程 A 使用\n之后整体交给线程 B 使用\n但同一时刻竞争并不强\n",[119,1849,1847],{"__ignoreMap":210},[196,1851,1852],{"id":1852},"批量撤销",[18,1854,1855],{},"如果该类持续表现出多线程竞争，偏向锁已经不再适合，JVM 可以撤销该类的偏向能力。之后新对象不再默认可偏向，已有对象也会按正常锁路径处理。",[18,1857,1858],{},"这也是为什么偏向锁不能只按“对象自身的四级升级”理解：它还存在以类为粒度的 prototype header、epoch 和批量启发式机制。",[14,1860,1862],{"id":1861},"九普通未锁定到轻量级锁","九、普通未锁定到轻量级锁",[18,1864,1865,1866,1869,1870,1873,1874,1876],{},"当对象处于普通未锁定状态，线程进入同步块时会在当前栈帧创建锁记录，HotSpot 源码中对应 ",[119,1867,1868],{},"BasicLock","，解释器栈中通常由 ",[119,1871,1872],{},"BasicObjectLock"," 将对象引用和 ",[119,1875,1868],{}," 组织在一起。",[18,1878,1879],{},"概念流程如下：",[204,1881,1884],{"className":1882,"code":1883,"language":209,"meta":210},[207],"1. 在线程栈创建 Lock Record\n2. 将对象原 Mark Word 保存到 displaced header\n3. CAS 修改对象 Mark Word\n4. 让 Mark Word 指向当前 Lock Record\n",[119,1885,1883],{"__ignoreMap":210},[18,1887,1888],{},"CAS 成功后：",[204,1890,1893],{"className":1891,"code":1892,"language":209,"meta":210},[207],"对象 Mark Word  →  线程栈 Lock Record\nLock Record     →  保存原 Mark Word\n",[119,1894,1892],{"__ignoreMap":210},[18,1896,1897],{},"对象头低两位为：",[204,1899,1902],{"className":1900,"code":1901,"language":209,"meta":210},[207],"00\n",[119,1903,1901],{"__ignoreMap":210},[18,1905,1906],{},"这就是通常所说的轻量级锁或栈锁。",[196,1908,1910],{"id":1909},"为什么要保存-displaced-header","为什么要保存 displaced header",[18,1912,1913],{},"轻量级锁占用了对象原来的 Mark Word。解锁时需要把原 Mark Word 恢复回对象头，因此先把它保存到 Lock Record。",[14,1915,1917],{"id":1916},"十轻量级锁的可重入","十、轻量级锁的可重入",[18,1919,1920],{},"同一个线程再次获取已经由自己栈锁定的对象时，JVM 会识别对象头中的 Lock Record 指针属于当前线程栈。",[18,1922,1923],{},"此时新的 Lock Record 会使用特殊值表示递归进入，例如 displaced header 置空，而不是再次覆盖对象头。",[18,1925,1926],{},"退出时：",[204,1928,1931],{"className":1929,"code":1930,"language":209,"meta":210},[207],"递归层 Lock Record：只退出当前递归层\n最外层 Lock Record：负责真正恢复对象头\n",[119,1932,1930],{"__ignoreMap":210},[18,1934,1935,1936,1938],{},"这解释了为什么 ",[119,1937,1233],{}," 天然支持可重入。",[14,1940,1942],{"id":1941},"十一轻量级锁如何释放","十一、轻量级锁如何释放",[18,1944,1945],{},"最外层同步块退出时，线程使用 CAS 尝试把 Lock Record 中保存的 displaced header 恢复到对象 Mark Word：",[204,1947,1950],{"className":1948,"code":1949,"language":209,"meta":210},[207],"期望值：对象头仍指向当前 Lock Record\n新值：进入同步块前保存的原 Mark Word\n",[119,1951,1949],{"__ignoreMap":210},[18,1953,1954],{},"CAS 成功：",[204,1956,1959],{"className":1957,"code":1958,"language":209,"meta":210},[207],"轻量级锁正常释放\n对象恢复普通未锁定状态\n",[119,1960,1958],{"__ignoreMap":210},[18,1962,1963],{},"CAS 失败通常意味着对象已在竞争过程中膨胀，不能再直接把原对象头覆盖回去，需要走 ObjectMonitor 的退出逻辑。",[18,1965,1966],{},"这就是“锁只能升级不能降级”表述不够准确的直接例子：没有发生膨胀的轻量级锁，退出后会恢复原 Mark Word。",[14,1968,1970],{"id":1969},"十二轻量级锁何时膨胀","十二、轻量级锁何时膨胀",[18,1972,1973],{},"线程尝试用 CAS 建立轻量级锁失败，说明对象头已经发生变化。可能原因包括：",[174,1975,1976,1979,1982,1985],{},[25,1977,1978],{},"另一个线程持有轻量级锁；",[25,1980,1981],{},"对象已经膨胀为 ObjectMonitor；",[25,1983,1984],{},"竞争期间其他线程正在处理锁状态；",[25,1986,1987],{},"必须执行依赖 ObjectMonitor 的操作。",[18,1989,1990],{},"JVM 可以先进行短暂自旋，希望持锁线程很快退出。自旋避免了线程立即挂起和恢复，但会消耗 CPU，因此只适合临界区较短的情况。",[18,1992,1993],{},"当快速路径不能解决问题时，JVM 会执行 Monitor inflation：",[204,1995,1998],{"className":1996,"code":1997,"language":209,"meta":210},[207],"创建或取得 ObjectMonitor\n复制并保存原 Mark Word\n将对象头改为指向 ObjectMonitor\n由 ObjectMonitor 负责后续竞争\n",[119,1999,1997],{"__ignoreMap":210},[18,2001,2002],{},"对象头低两位变成：",[204,2004,2007],{"className":2005,"code":2006,"language":209,"meta":210},[207],"10\n",[119,2008,2006],{"__ignoreMap":210},[14,2010,2012],{"id":2011},"十三重量级锁与-objectmonitor","十三、重量级锁与 ObjectMonitor",[18,2014,2015,2016,2019],{},"重量级状态下，对象 Mark Word 指向一个 ",[119,2017,2018],{},"ObjectMonitor","。理解它时重点关注以下字段或逻辑角色：",[204,2021,2024],{"className":2022,"code":2023,"language":209,"meta":210},[207],"_owner       当前持有 Monitor 的线程或相关锁记录\n_recursions  重入次数\n_cxq         新到达竞争线程形成的竞争队列\n_EntryList   等待重新竞争 Owner 的线程\n_WaitSet     调用 wait 后等待通知的线程\n_header      膨胀前保存的对象 Mark Word\n",[119,2025,2023],{"__ignoreMap":210},[18,2027,2028],{},"概念结构：",[204,2030,2033],{"className":2031,"code":2032,"language":209,"meta":210},[207],"对象 Mark Word\n      │\n      ▼\nObjectMonitor\n  ├─ Owner\n  ├─ Recursions\n  ├─ cxq \u002F EntryList\n  ├─ WaitSet\n  └─ displaced header\n",[119,2034,2032],{"__ignoreMap":210},[18,2036,2037],{},"竞争线程可能先自旋尝试获得 Owner。仍然失败时，线程会进入等待结构并被挂起；持有线程退出后，Monitor 选择或唤醒后继线程继续竞争。",[18,2039,2040],{},"“重量级”的成本主要来自：",[174,2042,2043,2046,2049,2052],{},[25,2044,2045],{},"维护竞争和等待队列；",[25,2047,2048],{},"线程阻塞、唤醒与调度；",[25,2050,2051],{},"高竞争下的上下文切换；",[25,2053,2054],{},"对 Monitor 状态的原子协调。",[18,2056,2057],{},"它并不意味着每次操作都必然立刻执行一次昂贵的系统调用，HotSpot 仍包含快速路径和自适应自旋等优化。",[14,2059,2061],{"id":2060},"十四waitnotify-为什么会涉及重量级-monitor","十四、wait、notify 为什么会涉及重量级 Monitor",[18,2063,2064,2066],{},[119,2065,1664],{}," 的语义不是普通锁竞争：",[204,2068,2071],{"className":2069,"code":2070,"language":209,"meta":210},[207],"确认当前线程持有 Monitor\n完整释放锁及其重入层数\n进入 WaitSet\n等待 notify、notifyAll、中断或超时\n重新参与锁竞争\n恢复原重入状态后返回\n",[119,2072,2070],{"__ignoreMap":210},[18,2074,2075,2076,2078],{},"这需要 Owner、WaitSet、EntryList、重入次数等完整 Monitor 能力，因此 JDK 8 HotSpot 执行 ",[119,2077,1664],{}," 时会确保对象已经膨胀为 ObjectMonitor。",[18,2080,2081,2084,2085,2088],{},[119,2082,2083],{},"notify()"," 和 ",[119,2086,2087],{},"notifyAll()"," 存在一个细节：如果对象当前只是由调用线程栈锁定，那么它不可能已经拥有 WaitSet，HotSpot 可以直接返回而不做无意义的膨胀；否则会取得或膨胀 ObjectMonitor，再处理等待线程。",[18,2090,2091,2093],{},[119,2092,2083],{}," 只是把等待线程从“等待条件”推进到“可以重新竞争锁”的阶段，并不会让它绕过当前 Owner 立即执行。",[14,2095,2097],{"id":2096},"十五identityhashcode-对锁状态的影响","十五、identityHashCode 对锁状态的影响",[18,2099,2100],{},"普通未锁定对象可以把 identity hash 保存在 Mark Word 中，但偏向锁的 Mark Word 需要保存 JavaThread 指针。",[18,2102,2103],{},"因此，对偏向对象计算：",[204,2105,2107],{"className":454,"code":2106,"language":456,"meta":210,"style":210},"System.identityHashCode(obj);\n",[119,2108,2109],{"__ignoreMap":210},[460,2110,2111],{"class":462,"line":463},[460,2112,2106],{},[18,2114,2115],{},"通常会导致偏向撤销，使对象头能够保存 hash。",[18,2117,2118],{},"如果对象正在使用轻量级锁，原 Mark Word 已经保存在当前线程栈的 Lock Record 中。为了稳定保存 identity hash，并让其他线程可靠读取，HotSpot 的相关路径可能把对象膨胀为 ObjectMonitor，再把 hash 保存在 Monitor 保存的 header 中。",[18,2120,2121],{},"所以观察锁升级实验时，不要在不知情的情况下调用可能计算 identity hash 的方法，否则实验本身会改变对象头。",[14,2123,2125],{"id":2124},"十六完整状态转换表","十六、完整状态转换表",[92,2127,2128,2141],{},[95,2129,2130],{},[98,2131,2132,2135,2138],{},[101,2133,2134],{},"当前状态",[101,2136,2137],{},"触发条件",[101,2139,2140],{},"可能结果",[111,2142,2143,2156,2169,2182,2194,2206,2218,2231,2243,2255,2267,2278,2291],{},[98,2144,2145,2150,2153],{},[116,2146,2147,2148],{},"普通未锁定 ",[119,2149,1459],{},[116,2151,2152],{},"首次进入同步块",[116,2154,2155],{},"CAS 成功后成为轻量级锁",[98,2157,2158,2163,2166],{},[116,2159,2160,2161],{},"匿名偏向 ",[119,2162,1480],{},[116,2164,2165],{},"第一个线程进入",[116,2167,2168],{},"CAS 写入线程指针，成为已偏向状态",[98,2170,2171,2176,2179],{},[116,2172,2173,2174],{},"已偏向 ",[119,2175,1480],{},[116,2177,2178],{},"原线程再次进入",[116,2180,2181],{},"保持偏向，低成本重入",[98,2183,2184,2188,2191],{},[116,2185,2173,2186],{},[119,2187,1480],{},[116,2189,2190],{},"新线程访问且 epoch 过期",[116,2192,2193],{},"尝试重偏向或撤销",[98,2195,2196,2200,2203],{},[116,2197,2173,2198],{},[119,2199,1480],{},[116,2201,2202],{},"新线程访问，原线程未持锁",[116,2204,2205],{},"撤销为普通状态，或按策略重偏向",[98,2207,2208,2212,2215],{},[116,2209,2173,2210],{},[119,2211,1480],{},[116,2213,2214],{},"新线程访问，原线程仍持锁",[116,2216,2217],{},"撤销偏向，转轻量级或膨胀",[98,2219,2220,2225,2228],{},[116,2221,2222,2223],{},"轻量级 ",[119,2224,1504],{},[116,2226,2227],{},"当前线程重入",[116,2229,2230],{},"新增递归 Lock Record，保持轻量级",[98,2232,2233,2237,2240],{},[116,2234,2222,2235],{},[119,2236,1504],{},[116,2238,2239],{},"无竞争退出",[116,2241,2242],{},"CAS 恢复原 Mark Word",[98,2244,2245,2249,2252],{},[116,2246,2222,2247],{},[119,2248,1504],{},[116,2250,2251],{},"竞争持续或恢复对象头失败",[116,2253,2254],{},"膨胀为 ObjectMonitor",[98,2256,2257,2260,2265],{},[116,2258,2259],{},"任意适用状态",[116,2261,2262,2264],{},[119,2263,1664],{}," 等需要完整 Monitor 语义",[116,2266,2254],{},[98,2268,2269,2272,2275],{},[116,2270,2271],{},"偏向\u002F轻量级",[116,2273,2274],{},"计算 identity hash 等特殊场景",[116,2276,2277],{},"撤销偏向或膨胀",[98,2279,2280,2285,2288],{},[116,2281,2282,2283],{},"重量级 ",[119,2284,1522],{},[116,2286,2287],{},"Owner 退出",[116,2289,2290],{},"唤醒\u002F选择后继；对象仍可保持膨胀",[98,2292,2293,2296,2299],{},[116,2294,2295],{},"空闲 ObjectMonitor",[116,2297,2298],{},"后续安全点清理",[116,2300,2301],{},"JVM 可能执行 Monitor deflation",[14,2303,2305],{"id":2304},"十七三条典型执行路径","十七、三条典型执行路径",[196,2307,2309],{"id":2308},"路径一同一个线程反复进入","路径一：同一个线程反复进入",[204,2311,2314],{"className":2312,"code":2313,"language":209,"meta":210},[207],"匿名偏向\n  → CAS 偏向线程 A\n  → A 执行同步块\n  → A 退出但保留偏向\n  → A 再次进入，快速命中\n",[119,2315,2313],{"__ignoreMap":210},[196,2317,2319],{"id":2318},"路径二线程交替使用没有重叠竞争","路径二：线程交替使用，没有重叠竞争",[18,2321,2322],{},"偏向关闭或偏向已撤销时：",[204,2324,2327],{"className":2325,"code":2326,"language":209,"meta":210},[207],"普通未锁定\n  → A 建立轻量级锁\n  → A CAS 恢复对象头\n  → B 建立轻量级锁\n  → B CAS 恢复对象头\n",[119,2328,2326],{"__ignoreMap":210},[18,2330,2331],{},"这种场景没有必要让线程进入阻塞等待。",[196,2333,2335],{"id":2334},"路径三多线程同时竞争","路径三：多线程同时竞争",[204,2337,2340],{"className":2338,"code":2339,"language":209,"meta":210},[207],"A 持有轻量级锁\n  → B CAS 失败并短暂自旋\n  → A 未及时释放\n  → 对象膨胀为 ObjectMonitor\n  → B 进入竞争\u002F等待结构\n  → A 退出并推进后继竞争\n",[119,2341,2339],{"__ignoreMap":210},[14,2343,2345],{"id":2344},"十八jol-验证时要注意什么","十八、JOL 验证时要注意什么",[18,2347,2348],{},"可以使用 JOL 查看对象布局：",[204,2350,2354],{"className":2351,"code":2352,"language":2353,"meta":210,"style":210},"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",[119,2355,2356,2361,2366,2371,2377],{"__ignoreMap":210},[460,2357,2358],{"class":462,"line":463},[460,2359,2360],{},"\u003Cdependency>\n",[460,2362,2363],{"class":462,"line":469},[460,2364,2365],{},"    \u003CgroupId>org.openjdk.jol\u003C\u002FgroupId>\n",[460,2367,2368],{"class":462,"line":1158},[460,2369,2370],{},"    \u003CartifactId>jol-core\u003C\u002FartifactId>\n",[460,2372,2374],{"class":462,"line":2373},4,[460,2375,2376],{},"    \u003Cversion>0.17\u003C\u002Fversion>\n",[460,2378,2380],{"class":462,"line":2379},5,[460,2381,2382],{},"\u003C\u002Fdependency>\n",[18,2384,2385],{},"示例：",[204,2387,2389],{"className":454,"code":2388,"language":456,"meta":210,"style":210},"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",[119,2390,2391,2396,2401,2406,2410,2415,2421,2427,2432],{"__ignoreMap":210},[460,2392,2393],{"class":462,"line":463},[460,2394,2395],{},"Object lock = new Object();\n",[460,2397,2398],{"class":462,"line":469},[460,2399,2400],{"emptyLinePlaceholder":1211},"\n",[460,2402,2403],{"class":462,"line":1158},[460,2404,2405],{},"System.out.println(ClassLayout.parseInstance(lock).toPrintable());\n",[460,2407,2408],{"class":462,"line":2373},[460,2409,2400],{"emptyLinePlaceholder":1211},[460,2411,2412],{"class":462,"line":2379},[460,2413,2414],{},"synchronized (lock) {\n",[460,2416,2418],{"class":462,"line":2417},6,[460,2419,2420],{},"    System.out.println(ClassLayout.parseInstance(lock).toPrintable());\n",[460,2422,2424],{"class":462,"line":2423},7,[460,2425,2426],{},"}\n",[460,2428,2430],{"class":462,"line":2429},8,[460,2431,2400],{"emptyLinePlaceholder":1211},[460,2433,2435],{"class":462,"line":2434},9,[460,2436,2405],{},[18,2438,2439],{},"实验时要控制以下变量：",[174,2441,2442,2445,2451,2457,2460,2463],{},[25,2443,2444],{},"明确使用 JDK 8 HotSpot；",[25,2446,2447,2448,1426],{},"记录是否开启 ",[119,2449,2450],{},"UseBiasedLocking",[25,2452,2453,2454,1426],{},"记录 ",[119,2455,2456],{},"BiasedLockingStartupDelay",[25,2458,2459],{},"避免日志、调试器或工具意外计算 identity hash；",[25,2461,2462],{},"JOL 输出会受 32\u002F64 位、压缩指针和 GC 配置影响；",[25,2464,2465],{},"线程已经终止不代表任何场景都一定重偏向，仍要看 epoch 和撤销策略。",[18,2467,2468],{},"为了避免默认 4 秒延迟影响实验，可以显式设置：",[204,2470,2473],{"className":2471,"code":2472,"language":209,"meta":210},[207],"-XX:+UseBiasedLocking\n-XX:BiasedLockingStartupDelay=0\n",[119,2474,2472],{"__ignoreMap":210},[18,2476,2477],{},"关闭偏向锁、直接观察轻量级路径：",[204,2479,2482],{"className":2480,"code":2481,"language":209,"meta":210},[207],"-XX:-UseBiasedLocking\n",[119,2483,2481],{"__ignoreMap":210},[14,2485,2487],{"id":2486},"十九常见误区","十九、常见误区",[196,2489,2491],{"id":2490},"误区一新对象一定先是无锁再升级为偏向锁","误区一：新对象一定先是无锁，再升级为偏向锁",[18,2493,2494],{},"不准确。偏向能力启用后，支持偏向的类可以让新对象直接使用匿名偏向的 prototype header。",[196,2496,2498],{"id":2497},"误区二第二个线程出现就一定升级为重量级锁","误区二：第二个线程出现就一定升级为重量级锁",[18,2500,2501],{},"不准确。它可能触发重偏向、偏向撤销、轻量级锁，也可能在存在实际竞争时膨胀。",[196,2503,2505],{"id":2504},"误区三轻量级锁就是一直自旋","误区三：轻量级锁就是一直自旋",[18,2507,2508],{},"不准确。轻量级锁的核心是栈上 Lock Record 与对象头 CAS；自旋是竞争时可能采用的等待优化，不是轻量级锁的完整定义。",[196,2510,2512],{"id":2511},"误区四重量级锁完全由操作系统-mutex-等同实现","误区四：重量级锁完全由操作系统 Mutex 等同实现",[18,2514,2515],{},"过度简化。ObjectMonitor 是 HotSpot 的 JVM 级同步结构，内部会使用 CAS、自旋、队列、park\u002Funpark 等机制；线程最终阻塞和唤醒会依赖操作系统能力，但两者不能直接画等号。",[196,2517,2519],{"id":2518},"误区五锁一旦膨胀这个对象终身都是重量级锁","误区五：锁一旦膨胀，这个对象终身都是重量级锁",[18,2521,2522],{},"不准确。当前竞争阶段通常不会立即降级，但空闲 Monitor 后续可能由 JVM 在安全点执行 deflation。",[196,2524,2526],{"id":2525},"误区六这套流程适用于所有-jdk","误区六：这套流程适用于所有 JDK",[18,2528,2529],{},"不准确。本文讨论的是 JDK 8 HotSpot。偏向锁后来被默认禁用，现代 HotSpot 的锁实现也持续演进，因此理解具体机制时必须先限定版本。",[14,2531,2533],{"id":2532},"二十参考资料","二十、参考资料",[174,2535,2536,2543,2550,2557],{},[25,2537,2538],{},[70,2539,2542],{"href":2540,"rel":2541},"https:\u002F\u002Fgithub.com\u002Fopenjdk\u002Fjdk8u\u002Fblob\u002Fmaster\u002Fhotspot\u002Fsrc\u002Fshare\u002Fvm\u002Foops\u002FmarkOop.hpp",[1101],"JDK 8 HotSpot：markOop.hpp",[25,2544,2545],{},[70,2546,2549],{"href":2547,"rel":2548},"https:\u002F\u002Fgithub.com\u002Fopenjdk\u002Fjdk8u\u002Fblob\u002Fmaster\u002Fhotspot\u002Fsrc\u002Fshare\u002Fvm\u002Fruntime\u002Fsynchronizer.cpp",[1101],"JDK 8 HotSpot：synchronizer.cpp",[25,2551,2552],{},[70,2553,2556],{"href":2554,"rel":2555},"https:\u002F\u002Fgithub.com\u002Fopenjdk\u002Fjdk8u\u002Fblob\u002Fmaster\u002Fhotspot\u002Fsrc\u002Fshare\u002Fvm\u002Fruntime\u002FbiasedLocking.cpp",[1101],"JDK 8 HotSpot：biasedLocking.cpp",[25,2558,2559],{},[70,2560,2563],{"href":2561,"rel":2562},"https:\u002F\u002Fgithub.com\u002Fopenjdk\u002Fjdk8u\u002Fblob\u002Fmaster\u002Fhotspot\u002Fsrc\u002Fshare\u002Fvm\u002Fruntime\u002FobjectMonitor.hpp",[1101],"JDK 8 HotSpot：objectMonitor.hpp",[14,2565,2566],{"id":2566},"相关内容",[174,2568,2569],{},[25,2570,2571],{},[70,2572,2574],{"href":2573},"\u002Fwiki\u002Fjava\u002Fsynchronized-vs-reentrantlock\u002F","synchronized 和 ReentrantLock 的区别",[1146,2576,1148],{},{"title":210,"searchDepth":469,"depth":469,"links":2578},[2579,2580,2581,2582,2586,2591,2595,2599,2604,2608,2611,2612,2613,2614,2615,2616,2617,2618,2623,2624,2632,2633],{"id":1227,"depth":469,"text":1228},{"id":66,"depth":469,"text":66},{"id":1290,"depth":469,"text":1291},{"id":1373,"depth":469,"text":1374,"children":2583},[2584,2585],{"id":1409,"depth":1158,"text":1410},{"id":1576,"depth":1158,"text":1577},{"id":1638,"depth":469,"text":1639,"children":2587},[2588,2589,2590],{"id":1645,"depth":1158,"text":1645},{"id":1651,"depth":1158,"text":1652},{"id":1658,"depth":1158,"text":1658},{"id":1676,"depth":469,"text":1677,"children":2592},[2593,2594],{"id":1692,"depth":1158,"text":1692},{"id":1707,"depth":1158,"text":1707},{"id":1734,"depth":469,"text":1735,"children":2596},[2597,2598],{"id":1771,"depth":1158,"text":1771},{"id":1786,"depth":1158,"text":1786},{"id":1792,"depth":469,"text":1793,"children":2600},[2601,2602,2603],{"id":1802,"depth":1158,"text":1803},{"id":1809,"depth":1158,"text":1810},{"id":1816,"depth":1158,"text":1817},{"id":1831,"depth":469,"text":1832,"children":2605},[2606,2607],{"id":1598,"depth":1158,"text":1598},{"id":1852,"depth":1158,"text":1852},{"id":1861,"depth":469,"text":1862,"children":2609},[2610],{"id":1909,"depth":1158,"text":1910},{"id":1916,"depth":469,"text":1917},{"id":1941,"depth":469,"text":1942},{"id":1969,"depth":469,"text":1970},{"id":2011,"depth":469,"text":2012},{"id":2060,"depth":469,"text":2061},{"id":2096,"depth":469,"text":2097},{"id":2124,"depth":469,"text":2125},{"id":2304,"depth":469,"text":2305,"children":2619},[2620,2621,2622],{"id":2308,"depth":1158,"text":2309},{"id":2318,"depth":1158,"text":2319},{"id":2334,"depth":1158,"text":2335},{"id":2344,"depth":469,"text":2345},{"id":2486,"depth":469,"text":2487,"children":2625},[2626,2627,2628,2629,2630,2631],{"id":2490,"depth":1158,"text":2491},{"id":2497,"depth":1158,"text":2498},{"id":2504,"depth":1158,"text":2505},{"id":2511,"depth":1158,"text":2512},{"id":2518,"depth":1158,"text":2519},{"id":2525,"depth":1158,"text":2526},{"id":2532,"depth":469,"text":2533},{"id":2566,"depth":469,"text":2566},"wiki:java:jdk8-synchronized-lock-states","从 Mark Word、Lock Record 与 ObjectMonitor 出发，系统理解 JDK 8 HotSpot 中偏向锁、轻量级锁、重量级锁的获取、撤销、膨胀和释放过程。","advanced",{},35,"\u002Fjava\u002Fjdk8-synchronized-lock-states","并发编程",{"title":1222,"description":2635},"java\u002Fjdk8-synchronized-lock-states","2026-07-23","o8PHY3tOTf0rejJ23m3VdGgkxtPD3RVAY71ujC1dbCs",{"id":2646,"title":2574,"body":2647,"commentId":3554,"description":3555,"difficulty":1207,"draft":1208,"extension":1209,"meta":3556,"navigation":1211,"order":3557,"path":3558,"section":1214,"seo":3559,"stem":3560,"updated":3561,"__hash__":3562},"java\u002Fjava\u002Fsynchronized-vs-reentrantlock.md",{"type":7,"value":2648,"toc":3532},[2649,2652,2655,2660,2663,2670,2672,2681,2683,2691,2701,2705,2707,2713,2716,2722,2725,2728,2734,2741,2744,2748,2901,2903,2907,2910,2915,2918,2924,2927,2930,2936,2939,2941,2945,2951,2954,2957,2963,2966,2971,2973,2977,2980,2983,2989,2992,2995,3001,3004,3010,3012,3018,3021,3023,3027,3030,3036,3039,3045,3050,3053,3055,3059,3064,3067,3073,3079,3085,3088,3094,3097,3103,3106,3112,3115,3121,3124,3130,3133,3135,3139,3142,3145,3151,3154,3160,3163,3166,3169,3175,3178,3181,3187,3190,3196,3203,3205,3209,3214,3220,3223,3226,3232,3239,3245,3248,3254,3257,3259,3263,3266,3269,3285,3288,3293,3296,3298,3302,3305,3325,3327,3333,3336,3341,3343,3347,3349,3375,3378,3384,3387,3389,3393,3438,3441,3446,3453,3459,3462,3464,3468,3472,3475,3479,3482,3486,3489,3497,3501,3504,3508,3511,3514,3520,3522,3525],[18,2650,2651],{},"synchronized和reentrantLock的区别？",[10,2653,2654],{"id":2654},"面试标准回答",[57,2656,2657],{},[18,2658,2659],{},"synchronized 是 JVM 层面的内置锁，语法简单，进入同步块后自动加锁，退出时自动释放，支持可重入和内存可见性。ReentrantLock 是基于 AQS 实现的显式锁，同样支持可重入，但提供了公平锁、可响应中断获取锁、tryLock、超时获取以及多个 Condition 等高级能力。两者在现代 JDK 中性能差距通常不是主要选型依据；功能简单时优先 synchronized，需要精细控制时使用 ReentrantLock，并且必须在 finally 中释放锁。",[18,2661,2662],{},"一句话记忆：",[57,2664,2665],{},[18,2666,2667],{},[28,2668,2669],{},"synchronized 简单自动，ReentrantLock 灵活可控。",[14,2671,66],{"id":66},[18,2673,2674],{},[70,2675,2678],{"href":2676,"target":73,"rel":2677},"\u002Fimages\u002Fwiki\u002Fjava\u002Fsynchronized-vs-reentrantlock.svg",[75,76],[78,2679],{"src":2676,"alt":2680},"synchronized 与 ReentrantLock 核心差异和选型建议",[10,2682,83],{"id":83},[18,2684,2685,2084,2687,2690],{},[119,2686,1233],{},[119,2688,2689],{},"ReentrantLock"," 都能实现互斥和可见性，但定位不同：",[57,2692,2693],{},[18,2694,2695,2697,2698,2700],{},[119,2696,1233],{}," 是 JVM 原生关键字，语法简单；",[119,2699,2689],{}," 是 JUC 提供的显式锁，功能更丰富、控制能力更强。",[14,2702,2704],{"id":2703},"一基本用法","一、基本用法",[196,2706,1233],{"id":1233},[204,2708,2711],{"className":2709,"code":2710,"language":209},[207],"synchronized (lock) {\n    \u002F\u002F 临界区\n}\n",[119,2712,2710],{"__ignoreMap":210},[18,2714,2715],{},"或者：",[204,2717,2720],{"className":2718,"code":2719,"language":209},[207],"public synchronized void method() {\n}\n",[119,2721,2719],{"__ignoreMap":210},[18,2723,2724],{},"锁会自动释放。",[196,2726,2689],{"id":2727},"reentrantlock",[204,2729,2732],{"className":2730,"code":2731,"language":209},[207],"ReentrantLock lock = new ReentrantLock();\n\nlock.lock();\ntry {\n    \u002F\u002F 临界区\n} finally {\n    lock.unlock();\n}\n",[119,2733,2731],{"__ignoreMap":210},[18,2735,2736,2737,2740],{},"必须手动释放，所以通常必须写在 ",[119,2738,2739],{},"finally"," 中。",[2742,2743],"hr",{},[14,2745,2747],{"id":2746},"二核心区别","二、核心区别",[92,2749,2750,2761],{},[95,2751,2752],{},[98,2753,2754,2757,2759],{},[101,2755,2756],{},"对比项",[101,2758,1233],{},[101,2760,2689],{},[111,2762,2763,2774,2785,2795,2805,2818,2830,2842,2853,2868,2879,2890],{},[98,2764,2765,2768,2771],{},[116,2766,2767],{},"实现层次",[116,2769,2770],{},"JVM 关键字、Monitor",[116,2772,2773],{},"Java 类，基于 AQS",[98,2775,2776,2779,2782],{},[116,2777,2778],{},"加锁释放",[116,2780,2781],{},"自动",[116,2783,2784],{},"手动",[98,2786,2787,2790,2793],{},[116,2788,2789],{},"可重入",[116,2791,2792],{},"支持",[116,2794,2792],{},[98,2796,2797,2800,2803],{},[116,2798,2799],{},"公平锁",[116,2801,2802],{},"不支持显式配置",[116,2804,2792],{},[98,2806,2807,2810,2813],{},[116,2808,2809],{},"可中断获取锁",[116,2811,2812],{},"不支持",[116,2814,2815],{},[119,2816,2817],{},"lockInterruptibly()",[98,2819,2820,2823,2825],{},[116,2821,2822],{},"尝试获取锁",[116,2824,2812],{},[116,2826,2827],{},[119,2828,2829],{},"tryLock()",[98,2831,2832,2835,2837],{},[116,2833,2834],{},"超时获取锁",[116,2836,2812],{},[116,2838,2839],{},[119,2840,2841],{},"tryLock(timeout, unit)",[98,2843,2844,2847,2850],{},[116,2845,2846],{},"条件队列",[116,2848,2849],{},"一个 Monitor WaitSet",[116,2851,2852],{},"可创建多个 Condition",[98,2854,2855,2858,2863],{},[116,2856,2857],{},"等待通知",[116,2859,2860],{},[119,2861,2862],{},"wait\u002Fnotify\u002FnotifyAll",[116,2864,2865],{},[119,2866,2867],{},"await\u002Fsignal\u002FsignalAll",[98,2869,2870,2873,2876],{},[116,2871,2872],{},"锁状态查询",[116,2874,2875],{},"能力有限",[116,2877,2878],{},"提供较多查询方法",[98,2880,2881,2884,2887],{},[116,2882,2883],{},"编码复杂度",[116,2885,2886],{},"低",[116,2888,2889],{},"较高",[98,2891,2892,2895,2898],{},[116,2893,2894],{},"异常释放",[116,2896,2897],{},"自动释放",[116,2899,2900],{},"必须 finally unlock",[2742,2902],{},[10,2904,2906],{"id":2905},"三两者都支持可重入","三、两者都支持可重入",[18,2908,2909],{},"可重入指：",[57,2911,2912],{},[18,2913,2914],{},"同一个线程已经持有锁时，可以再次获取同一把锁。",[196,2916,1233],{"id":2917},"synchronized-1",[204,2919,2922],{"className":2920,"code":2921,"language":209},[207],"public synchronized void methodA() {\n    methodB();\n}\n\npublic synchronized void methodB() {\n}\n",[119,2923,2921],{"__ignoreMap":210},[18,2925,2926],{},"同一个对象上的两个同步方法，线程可以重入。",[196,2928,2689],{"id":2929},"reentrantlock-1",[204,2931,2934],{"className":2932,"code":2933,"language":209},[207],"lock.lock();\ntry {\n    lock.lock();\n    try {\n        \u002F\u002F 重入\n    } finally {\n        lock.unlock();\n    }\n} finally {\n    lock.unlock();\n}\n",[119,2935,2933],{"__ignoreMap":210},[18,2937,2938],{},"获取几次，就必须释放几次。",[2742,2940],{},[10,2942,2944],{"id":2943},"四reentrantlock-支持公平锁","四、ReentrantLock 支持公平锁",[204,2946,2949],{"className":2947,"code":2948,"language":209},[207],"ReentrantLock fairLock = new ReentrantLock(true);\n",[119,2950,2948],{"__ignoreMap":210},[18,2952,2953],{},"公平锁会尽量按 AQS 队列顺序获取锁。",[18,2955,2956],{},"默认是非公平锁：",[204,2958,2961],{"className":2959,"code":2960,"language":209},[207],"ReentrantLock lock = new ReentrantLock();\n",[119,2962,2960],{"__ignoreMap":210},[18,2964,2965],{},"非公平锁允许新线程插队，吞吐量通常更高。",[18,2967,2968,2970],{},[119,2969,1233],{}," 没有 API 让你指定公平性，通常按非公平竞争理解。",[2742,2972],{},[10,2974,2976],{"id":2975},"五reentrantlock-支持可响应中断等待","五、ReentrantLock 支持可响应中断等待",[18,2978,2979],{},"假设线程正在等待锁。",[18,2981,2982],{},"使用：",[204,2984,2987],{"className":2985,"code":2986,"language":209},[207],"lock.lock();\n",[119,2988,2986],{"__ignoreMap":210},[18,2990,2991],{},"即使线程被中断，也不会因为中断立即退出获取锁过程。",[18,2993,2994],{},"而：",[204,2996,2999],{"className":2997,"code":2998,"language":209},[207],"lock.lockInterruptibly();\n",[119,3000,2998],{"__ignoreMap":210},[18,3002,3003],{},"等待期间如果收到中断，会抛出：",[204,3005,3008],{"className":3006,"code":3007,"language":209},[207],"InterruptedException\n",[119,3009,3007],{"__ignoreMap":210},[18,3011,666],{},[204,3013,3016],{"className":3014,"code":3015,"language":209},[207],"try {\n    lock.lockInterruptibly();\n    try {\n        doWork();\n    } finally {\n        lock.unlock();\n    }\n} catch (InterruptedException e) {\n    Thread.currentThread().interrupt();\n}\n",[119,3017,3015],{"__ignoreMap":210},[18,3019,3020],{},"这对于避免线程长时间死等、响应取消请求很有价值。",[2742,3022],{},[10,3024,3026],{"id":3025},"六reentrantlock-支持尝试获取和超时","六、ReentrantLock 支持尝试获取和超时",[196,3028,3029],{"id":3029},"立即尝试",[204,3031,3034],{"className":3032,"code":3033,"language":209},[207],"if (lock.tryLock()) {\n    try {\n        doWork();\n    } finally {\n        lock.unlock();\n    }\n} else {\n    \u002F\u002F 获取失败，走降级逻辑\n}\n",[119,3035,3033],{"__ignoreMap":210},[196,3037,3038],{"id":3038},"超时尝试",[204,3040,3043],{"className":3041,"code":3042,"language":209},[207],"if (lock.tryLock(2, TimeUnit.SECONDS)) {\n    try {\n        doWork();\n    } finally {\n        lock.unlock();\n    }\n} else {\n    \u002F\u002F 两秒内未拿到锁\n}\n",[119,3044,3042],{"__ignoreMap":210},[18,3046,3047,3049],{},[119,3048,1233],{}," 一旦进入竞争，只能等待，无法直接设置超时。",[18,3051,3052],{},"这也是 ReentrantLock 在需要降级、超时控制时的重要优势。",[2742,3054],{},[10,3056,3058],{"id":3057},"七condition-比-waitnotify-更灵活","七、Condition 比 wait\u002Fnotify 更灵活",[18,3060,3061,3063],{},[119,3062,1233],{}," 的一个 Monitor 只有一个 WaitSet。",[18,3065,3066],{},"例如阻塞队列里：",[204,3068,3071],{"className":3069,"code":3070,"language":209},[207],"生产者等待 notFull\n消费者等待 notEmpty\n",[119,3072,3070],{"__ignoreMap":210},[18,3074,3075,3076,3078],{},"使用 ",[119,3077,1233],{}," 时，生产者和消费者都在同一个 WaitSet 中，通常要：",[204,3080,3083],{"className":3081,"code":3082,"language":209},[207],"notifyAll();\n",[119,3084,3082],{"__ignoreMap":210},[18,3086,3087],{},"而 ReentrantLock 可以创建多个 Condition：",[204,3089,3092],{"className":3090,"code":3091,"language":209},[207],"Condition notFull = lock.newCondition();\nCondition notEmpty = lock.newCondition();\n",[119,3093,3091],{"__ignoreMap":210},[18,3095,3096],{},"生产者等待：",[204,3098,3101],{"className":3099,"code":3100,"language":209},[207],"while (queue.size() == capacity) {\n    notFull.await();\n}\n",[119,3102,3100],{"__ignoreMap":210},[18,3104,3105],{},"消费者等待：",[204,3107,3110],{"className":3108,"code":3109,"language":209},[207],"while (queue.isEmpty()) {\n    notEmpty.await();\n}\n",[119,3111,3109],{"__ignoreMap":210},[18,3113,3114],{},"生产完成：",[204,3116,3119],{"className":3117,"code":3118,"language":209},[207],"notEmpty.signal();\n",[119,3120,3118],{"__ignoreMap":210},[18,3122,3123],{},"消费完成：",[204,3125,3128],{"className":3126,"code":3127,"language":209},[207],"notFull.signal();\n",[119,3129,3127],{"__ignoreMap":210},[18,3131,3132],{},"这样可以精确唤醒，减少无效唤醒。",[2742,3134],{},[10,3136,3138],{"id":3137},"八底层实现不同","八、底层实现不同",[14,3140,1233],{"id":3141},"synchronized-2",[18,3143,3144],{},"经典理解：",[204,3146,3149],{"className":3147,"code":3148,"language":209},[207],"对象头 Mark Word\n+\nMonitor\n+\nJVM 锁优化\n",[119,3150,3148],{"__ignoreMap":210},[18,3152,3153],{},"JDK 8 中可能经历：",[204,3155,3158],{"className":3156,"code":3157,"language":209},[207],"偏向锁\n→ 轻量级锁\n→ 重量级锁\n",[119,3159,3157],{"__ignoreMap":210},[18,3161,3162],{},"这是 HotSpot 的 JVM 实现优化。",[14,3164,2689],{"id":3165},"reentrantlock-2",[18,3167,3168],{},"主要基于：",[204,3170,3173],{"className":3171,"code":3172,"language":209},[207],"AbstractQueuedSynchronizer\n",[119,3174,3172],{"__ignoreMap":210},[18,3176,3177],{},"也就是 AQS。",[18,3179,3180],{},"核心状态：",[204,3182,3185],{"className":3183,"code":3184,"language":209},[207],"volatile int state;\n",[119,3186,3184],{"__ignoreMap":210},[18,3188,3189],{},"独占锁时：",[204,3191,3194],{"className":3192,"code":3193,"language":209},[207],"state = 0  未加锁\nstate = 1  第一次获取\nstate > 1  重入次数\n",[119,3195,3193],{"__ignoreMap":210},[18,3197,3198,3199,3202],{},"竞争失败的线程进入 CLH 变体同步队列，并通过 ",[119,3200,3201],{},"park\u002Funpark"," 等待和唤醒。",[2742,3204],{},[10,3206,3208],{"id":3207},"九异常情况下的差异","九、异常情况下的差异",[18,3210,3211,3213],{},[119,3212,1233],{},"：",[204,3215,3218],{"className":3216,"code":3217,"language":209},[207],"synchronized (lock) {\n    throw new RuntimeException();\n}\n",[119,3219,3217],{"__ignoreMap":210},[18,3221,3222],{},"离开同步块时，JVM 自动释放锁。",[18,3224,3225],{},"ReentrantLock：",[204,3227,3230],{"className":3228,"code":3229,"language":209},[207],"lock.lock();\ndoWork();\nlock.unlock();\n",[119,3231,3229],{"__ignoreMap":210},[18,3233,3234,3235,3238],{},"如果 ",[119,3236,3237],{},"doWork()"," 抛异常：",[204,3240,3243],{"className":3241,"code":3242,"language":209},[207],"unlock 没执行\n→ 锁永久未释放\n",[119,3244,3242],{"__ignoreMap":210},[18,3246,3247],{},"所以必须写：",[204,3249,3252],{"className":3250,"code":3251,"language":209},[207],"lock.lock();\ntry {\n    doWork();\n} finally {\n    lock.unlock();\n}\n",[119,3253,3251],{"__ignoreMap":210},[18,3255,3256],{},"这是 ReentrantLock 最常见的编码风险。",[2742,3258],{},[10,3260,3262],{"id":3261},"十性能区别","十、性能区别",[18,3264,3265],{},"早期 JDK 中，ReentrantLock 性能常常明显优于 synchronized。",[18,3267,3268],{},"但 JDK 6 以后，JVM 对 synchronized 做了大量优化：",[174,3270,3271,3274,3276,3279,3282],{},[25,3272,3273],{},"偏向锁",[25,3275,1507],{},[25,3277,3278],{},"自旋",[25,3280,3281],{},"锁消除",[25,3283,3284],{},"锁粗化",[18,3286,3287],{},"现代 JDK 中：",[57,3289,3290],{},[18,3291,3292],{},"两者性能差异通常不是选型的首要依据。",[18,3294,3295],{},"应该根据功能需求选择，而不是简单认为 ReentrantLock 一定更快。",[2742,3297],{},[10,3299,3301],{"id":3300},"十一什么时候使用-synchronized","十一、什么时候使用 synchronized",[18,3303,3304],{},"适合：",[174,3306,3307,3310,3313,3316,3319,3322],{},[25,3308,3309],{},"临界区简单",[25,3311,3312],{},"不需要公平锁",[25,3314,3315],{},"不需要超时获取",[25,3317,3318],{},"不需要中断锁等待",[25,3320,3321],{},"只有一个等待条件",[25,3323,3324],{},"希望代码简单、自动释放锁",[18,3326,666],{},[204,3328,3331],{"className":3329,"code":3330,"language":209},[207],"public synchronized void increment() {\n    count++;\n}\n",[119,3332,3330],{"__ignoreMap":210},[18,3334,3335],{},"一般优先原则：",[57,3337,3338],{},[18,3339,3340],{},"能用 synchronized 清晰表达时，优先用 synchronized。",[2742,3342],{},[10,3344,3346],{"id":3345},"十二什么时候使用-reentrantlock","十二、什么时候使用 ReentrantLock",[18,3348,3304],{},[174,3350,3351,3354,3360,3363,3366,3369,3372],{},[25,3352,3353],{},"需要公平锁",[25,3355,3356,3357],{},"需要 ",[119,3358,3359],{},"tryLock",[25,3361,3362],{},"需要超时获取锁",[25,3364,3365],{},"需要可中断等待",[25,3367,3368],{},"需要多个 Condition",[25,3370,3371],{},"需要查询等待队列、持锁状态",[25,3373,3374],{},"需要更精细的锁控制",[18,3376,3377],{},"例如转账时避免死锁：",[204,3379,3382],{"className":3380,"code":3381,"language":209},[207],"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",[119,3383,3381],{"__ignoreMap":210},[18,3385,3386],{},"使用 synchronized 很难实现这种超时退避。",[2742,3388],{},[10,3390,3392],{"id":3391},"十三waitnotify-和-awaitsignal-对应关系","十三、wait\u002Fnotify 和 await\u002Fsignal 对应关系",[92,3394,3395,3403],{},[95,3396,3397],{},[98,3398,3399,3401],{},[101,3400,1233],{},[101,3402,2689],{},[111,3404,3405,3416,3427],{},[98,3406,3407,3411],{},[116,3408,3409],{},[119,3410,1664],{},[116,3412,3413],{},[119,3414,3415],{},"Condition.await()",[98,3417,3418,3422],{},[116,3419,3420],{},[119,3421,2083],{},[116,3423,3424],{},[119,3425,3426],{},"Condition.signal()",[98,3428,3429,3433],{},[116,3430,3431],{},[119,3432,2087],{},[116,3434,3435],{},[119,3436,3437],{},"Condition.signalAll()",[18,3439,3440],{},"两者都要求：",[57,3442,3443],{},[18,3444,3445],{},"调用等待或通知方法前，必须先持有对应的锁。",[18,3447,3448,3449,3452],{},"并且等待都应该放在 ",[119,3450,3451],{},"while"," 中：",[204,3454,3457],{"className":3455,"code":3456,"language":209},[207],"while (!conditionSatisfied()) {\n    condition.await();\n}\n",[119,3458,3456],{"__ignoreMap":210},[18,3460,3461],{},"防止虚假唤醒和竞争后条件失效。",[2742,3463],{},[10,3465,3467],{"id":3466},"十四常见误区","十四、常见误区",[196,3469,3471],{"id":3470},"误区一reentrantlock-才支持可重入","误区一：ReentrantLock 才支持可重入",[18,3473,3474],{},"不对。两者都可重入。",[196,3476,3478],{"id":3477},"误区二reentrantlock-一定比-synchronized-快","误区二：ReentrantLock 一定比 synchronized 快",[18,3480,3481],{},"不对。现代 JVM 下要看场景，功能差异比纯性能更重要。",[196,3483,3485],{"id":3484},"误区三synchronized-会自动释放reentrantlock-不会","误区三：synchronized 会自动释放，ReentrantLock 不会",[18,3487,3488],{},"更准确地说：",[174,3490,3491,3494],{},[25,3492,3493],{},"synchronized 离开同步块时自动释放",[25,3495,3496],{},"ReentrantLock 必须显式 unlock",[196,3498,3500],{"id":3499},"误区四公平锁一定更好","误区四：公平锁一定更好",[18,3502,3503],{},"公平锁吞吐量通常更低，只在有明确公平性需求时使用。",[196,3505,3507],{"id":3506},"误区五trylock-失败后可以直接-unlock","误区五：tryLock 失败后可以直接 unlock",[18,3509,3510],{},"不可以。只有成功获取锁后才能释放。",[18,3512,3513],{},"正确写法：",[204,3515,3518],{"className":3516,"code":3517,"language":209},[207],"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",[119,3519,3517],{"__ignoreMap":210},[2742,3521],{},[14,3523,3524],{"id":3524},"相关主题",[174,3526,3527],{},[25,3528,3529],{},[70,3530,1222],{"href":3531},"\u002Fwiki\u002Fjava\u002Fjdk8-synchronized-lock-states\u002F",{"title":210,"searchDepth":469,"depth":469,"links":3533},[3534,3535,3539,3545,3546,3553],{"id":66,"depth":469,"text":66},{"id":2703,"depth":469,"text":2704,"children":3536},[3537,3538],{"id":1233,"depth":1158,"text":1233},{"id":2727,"depth":1158,"text":2689},{"id":2746,"depth":469,"text":2747,"children":3540},[3541,3542,3543,3544],{"id":2917,"depth":1158,"text":1233},{"id":2929,"depth":1158,"text":2689},{"id":3029,"depth":1158,"text":3029},{"id":3038,"depth":1158,"text":3038},{"id":3141,"depth":469,"text":1233},{"id":3165,"depth":469,"text":2689,"children":3547},[3548,3549,3550,3551,3552],{"id":3470,"depth":1158,"text":3471},{"id":3477,"depth":1158,"text":3478},{"id":3484,"depth":1158,"text":3485},{"id":3499,"depth":1158,"text":3500},{"id":3506,"depth":1158,"text":3507},{"id":3524,"depth":469,"text":3524},"wiki:java:synchronized-vs-reentrantlock","对比 synchronized 与 ReentrantLock 的实现、可重入、公平锁、中断、超时、Condition 和适用场景。",{},71,"\u002Fjava\u002Fsynchronized-vs-reentrantlock",{"title":2574,"description":3555},"java\u002Fsynchronized-vs-reentrantlock","2026-07-21","HvwBdM7dDk9OpIeOwtk34xX3bS9W_vivGkHtOntEom0",{"id":3564,"title":3565,"body":3566,"commentId":4742,"description":4743,"difficulty":2636,"draft":1208,"extension":1209,"meta":4744,"navigation":1211,"order":4745,"path":4746,"section":1214,"seo":4747,"stem":4748,"updated":4749,"__hash__":4750},"java\u002Fjava\u002Fkafka-message-loss-prevention.md","Kafka 如何保证消息不丢失",{"type":7,"value":3567,"toc":4697},[3568,3571,3573,3590,3592,3599,3601,3610,3612,3615,3622,3625,3629,3632,3646,3649,3653,3659,3666,3721,3724,3735,3744,3748,3751,3780,3783,3786,3800,3804,3807,3816,3823,3858,3865,3869,3872,3878,3881,3884,3890,3893,3897,3901,3904,3910,3913,3916,3920,3923,3926,3932,3935,3941,3946,3952,3955,3961,3964,3969,3972,3976,3985,3991,3994,4003,4006,4012,4022,4027,4030,4034,4038,4047,4050,4056,4059,4063,4066,4072,4075,4121,4124,4128,4131,4137,4140,4157,4164,4168,4171,4177,4180,4189,4192,4195,4199,4202,4208,4212,4215,4218,4224,4227,4230,4250,4257,4260,4266,4272,4276,4279,4281,4287,4293,4296,4310,4314,4385,4388,4395,4399,4449,4452,4459,4462,4466,4576,4580,4587,4595,4599,4605,4609,4612,4616,4619,4623,4626,4630,4633,4653,4656,4658,4695],[10,3569,3565],{"id":3570},"kafka-如何保证消息不丢失",[14,3572,16],{"id":16},[57,3574,3575],{},[18,3576,3577,3578,3581,3582,3585,3586,3589],{},"Kafka 保证消息不丢失需要覆盖生产者、Broker 和消费者三段链路。生产者侧使用 ",[119,3579,3580],{},"acks=all","、开启重试和幂等生产，并检查发送回调，对最终失败进行落库、告警或补偿；Broker 侧通常设置三副本、",[119,3583,3584],{},"min.insync.replicas=2","，并关闭非同步副本选举，即配置 ",[119,3587,3588],{},"unclean.leader.election.enable=false","，保证消息在足够多的同步副本写入后才确认成功；消费者侧关闭自动提交，在业务处理成功后再提交 Offset，失败时不提交以便重新消费。这样通常实现至少一次投递，所以消费端还必须通过事件 ID、唯一索引或状态机保证幂等。涉及数据库与 Kafka 的一致性时，可以使用 Transactional Outbox；消费端需要应对重复消息时，可以使用 Inbox。Kafka 的消息不丢失是端到端保障，不是只配置一个参数。",[18,3591,55],{},[57,3593,3594],{},[18,3595,3596],{},[28,3597,3598],{},"生产者确认成功，Broker 多副本可靠保存，消费者处理成功后提交，业务端用幂等和补偿兜底。",[14,3600,66],{"id":66},[18,3602,3603],{},[70,3604,3607],{"href":3605,"target":73,"rel":3606},"\u002Fimages\u002Fwiki\u002Fjava\u002Fkafka-message-loss-prevention.svg",[75,76],[78,3608],{"src":3605,"alt":3609},"Kafka 消息不丢失的端到端保障链路",[14,3611,83],{"id":83},[18,3613,3614],{},"Kafka 消息不丢失不是靠某一个参数实现的，而是一个端到端问题：",[57,3616,3617],{},[18,3618,3619],{},[28,3620,3621],{},"生产者必须确认消息发送成功，Broker 必须把消息可靠地保存在足够多的副本中，消费者必须在业务处理成功后再提交 Offset。",[18,3623,3624],{},"其中任何一个环节处理不当，都可能出现“业务上看起来消息丢了”的情况。",[14,3626,3628],{"id":3627},"一先明确什么叫消息丢失","一、先明确什么叫“消息丢失”",[18,3630,3631],{},"常见的丢失场景主要有四类：",[22,3633,3634,3637,3640,3643],{},[25,3635,3636],{},"生产者调用发送方法后没有检查结果，实际发送失败却被业务忽略。",[25,3638,3639],{},"Leader 收到消息后就返回成功，消息还未复制到 Follower，Leader 随后故障。",[25,3641,3642],{},"消费者先提交 Offset，再执行业务；业务处理失败后，消息不会再次投递。",[25,3644,3645],{},"Kafka 中的消息没有丢，但写数据库、调用下游等业务操作失败，又没有补偿机制。",[18,3647,3648],{},"因此，Kafka 中仍能查到消息，不代表业务一定处理成功；反过来，业务没有结果，也不一定是 Broker 丢了消息。",[14,3650,3652],{"id":3651},"二生产者如何避免丢消息","二、生产者如何避免丢消息",[196,3654,3656,3657],{"id":3655},"_1-使用-acksall","1. 使用 ",[119,3658,3580],{},[18,3660,3661,3662,3665],{},"生产者的 ",[119,3663,3664],{},"acks"," 决定 Broker 在什么条件下确认发送成功：",[92,3667,3668,3681],{},[95,3669,3670],{},[98,3671,3672,3675,3678],{},[101,3673,3674],{},"配置",[101,3676,3677],{},"确认时机",[101,3679,3680],{},"风险",[111,3682,3683,3696,3709],{},[98,3684,3685,3690,3693],{},[116,3686,3687],{},[119,3688,3689],{},"acks=0",[116,3691,3692],{},"不等待 Broker 响应",[116,3694,3695],{},"发送失败也无法感知，风险最高",[98,3697,3698,3703,3706],{},[116,3699,3700],{},[119,3701,3702],{},"acks=1",[116,3704,3705],{},"Leader 写入本地日志后返回",[116,3707,3708],{},"Leader 故障且副本尚未同步时可能丢失",[98,3710,3711,3715,3718],{},[116,3712,3713],{},[119,3714,3580],{},[116,3716,3717],{},"当前 ISR 中的副本都确认后返回",[116,3719,3720],{},"Kafka 能提供的最强生产者确认保证",[18,3722,3723],{},"关键配置：",[204,3725,3729],{"className":3726,"code":3727,"language":3728,"meta":210,"style":210},"language-properties shiki shiki-themes github-light github-dark","acks=all\n","properties",[119,3730,3731],{"__ignoreMap":210},[460,3732,3733],{"class":462,"line":463},[460,3734,3727],{},[18,3736,3737,3739,3740,3743],{},[119,3738,3580],{}," 并不等于绝对不丢。它还需要与副本数和 ",[119,3741,3742],{},"min.insync.replicas"," 配合，否则 ISR 中只剩 Leader 一个副本时，依然可能成功写入。",[196,3745,3747],{"id":3746},"_2-开启重试和幂等生产","2. 开启重试和幂等生产",[18,3749,3750],{},"网络抖动、Leader 切换等临时故障可能导致发送失败，生产者需要允许重试：",[204,3752,3754],{"className":3726,"code":3753,"language":3728,"meta":210,"style":210},"enable.idempotence=true\nacks=all\nretries=2147483647\nmax.in.flight.requests.per.connection=5\ndelivery.timeout.ms=120000\n",[119,3755,3756,3761,3765,3770,3775],{"__ignoreMap":210},[460,3757,3758],{"class":462,"line":463},[460,3759,3760],{},"enable.idempotence=true\n",[460,3762,3763],{"class":462,"line":469},[460,3764,3727],{},[460,3766,3767],{"class":462,"line":1158},[460,3768,3769],{},"retries=2147483647\n",[460,3771,3772],{"class":462,"line":2373},[460,3773,3774],{},"max.in.flight.requests.per.connection=5\n",[460,3776,3777],{"class":462,"line":2379},[460,3778,3779],{},"delivery.timeout.ms=120000\n",[18,3781,3782],{},"幂等生产者会给消息附加 Producer ID 和序列号，使 Broker 能识别同一生产会话内的重复写入，避免因重试产生重复消息。",[18,3784,3785],{},"需要注意：",[174,3787,3788,3791,3797],{},[25,3789,3790],{},"幂等生产解决的是重试导致的重复写入，不是发送失败后的业务补偿。",[25,3792,3793,3796],{},[119,3794,3795],{},"delivery.timeout.ms"," 到期后，生产者仍可能最终失败。",[25,3798,3799],{},"业务不能无限依赖客户端重试，最终失败必须记录、告警或进入补偿流程。",[196,3801,3803],{"id":3802},"_3-必须检查发送结果","3. 必须检查发送结果",[18,3805,3806],{},"下面这种“只发送、不处理结果”的写法存在风险：",[204,3808,3810],{"className":454,"code":3809,"language":456,"meta":210,"style":210},"producer.send(record);\n",[119,3811,3812],{"__ignoreMap":210},[460,3813,3814],{"class":462,"line":463},[460,3815,3809],{},[18,3817,3818,3819,3822],{},"应该检查回调或 ",[119,3820,3821],{},"Future"," 的执行结果：",[204,3824,3826],{"className":454,"code":3825,"language":456,"meta":210,"style":210},"producer.send(record, (metadata, exception) -> {\n    if (exception != null) {\n        \u002F\u002F 记录原始消息、告警并进入补偿流程\n        handleSendFailure(record, exception);\n    }\n});\n",[119,3827,3828,3833,3838,3843,3848,3853],{"__ignoreMap":210},[460,3829,3830],{"class":462,"line":463},[460,3831,3832],{},"producer.send(record, (metadata, exception) -> {\n",[460,3834,3835],{"class":462,"line":469},[460,3836,3837],{},"    if (exception != null) {\n",[460,3839,3840],{"class":462,"line":1158},[460,3841,3842],{},"        \u002F\u002F 记录原始消息、告警并进入补偿流程\n",[460,3844,3845],{"class":462,"line":2373},[460,3846,3847],{},"        handleSendFailure(record, exception);\n",[460,3849,3850],{"class":462,"line":2379},[460,3851,3852],{},"    }\n",[460,3854,3855],{"class":462,"line":2417},[460,3856,3857],{},"});\n",[18,3859,3860,3861,3864],{},"序列化失败、鉴权失败、消息过大和超时等错误，最终都需要业务明确处理。不能把“调用过 ",[119,3862,3863],{},"send","”当成“消息已经可靠进入 Kafka”。",[196,3866,3868],{"id":3867},"_4-数据库与-kafka-的一致性","4. 数据库与 Kafka 的一致性",[18,3870,3871],{},"如果业务流程是：",[204,3873,3876],{"className":3874,"code":3875,"language":209,"meta":210},[207],"更新数据库\n→\n发送 Kafka 消息\n",[119,3877,3875],{"__ignoreMap":210},[18,3879,3880],{},"数据库更新成功、消息发送失败时，仍然会造成业务事件丢失。",[18,3882,3883],{},"常见解决方式是本地消息表，也叫 Transactional Outbox：",[204,3885,3888],{"className":3886,"code":3887,"language":209,"meta":210},[207],"同一个数据库事务\n├── 更新业务数据\n└── 写入待发送事件表\n\n事务提交后\n→ 后台任务投递 Kafka\n→ 成功后标记事件已发送\n",[119,3889,3887],{"__ignoreMap":210},[18,3891,3892],{},"这样即使 Kafka 暂时不可用，消息也可以从本地消息表继续重试。Kafka 事务可以保证 Kafka 内部多条记录及消费 Offset 的原子性，但不会自动把外部数据库事务包含进来。",[14,3894,3896],{"id":3895},"三broker-如何保证消息可靠保存","三、Broker 如何保证消息可靠保存",[196,3898,3900],{"id":3899},"_1-合理设置副本数","1. 合理设置副本数",[18,3902,3903],{},"生产环境通常使用：",[204,3905,3908],{"className":3906,"code":3907,"language":209,"meta":210},[207],"replication.factor = 3\n",[119,3909,3907],{"__ignoreMap":210},[18,3911,3912],{},"一个分区包含一个 Leader 和多个 Follower。生产者与 Leader 交互，Follower 从 Leader 复制日志。某个 Broker 故障后，可以从仍然同步的副本中选举新 Leader。",[18,3914,3915],{},"副本数提高的是容错能力，但副本只有真正保持同步才有意义。",[196,3917,3919],{"id":3918},"_2-理解-isr","2. 理解 ISR",[18,3921,3922],{},"ISR 是 In-Sync Replicas，即当前与 Leader 保持同步的副本集合。",[18,3924,3925],{},"例如一个三副本分区：",[204,3927,3930],{"className":3928,"code":3929,"language":209,"meta":210},[207],"Leader A\nFollower B\nFollower C\n\nISR = [A, B, C]\n",[119,3931,3929],{"__ignoreMap":210},[18,3933,3934],{},"如果 C 长时间跟不上 Leader，它会被移出 ISR：",[204,3936,3939],{"className":3937,"code":3938,"language":209,"meta":210},[207],"ISR = [A, B]\n",[119,3940,3938],{"__ignoreMap":210},[18,3942,3943,3945],{},[119,3944,3580],{}," 等待的是当前 ISR 的确认，而不是永远等待所有配置副本。",[196,3947,3949,3950],{"id":3948},"_3-配置-mininsyncreplicas","3. 配置 ",[119,3951,3742],{},[18,3953,3954],{},"推荐组合：",[204,3956,3959],{"className":3957,"code":3958,"language":209,"meta":210},[207],"replication.factor = 3\nmin.insync.replicas = 2\nacks = all\n",[119,3960,3958],{"__ignoreMap":210},[18,3962,3963],{},"它表达的含义是：",[57,3965,3966],{},[18,3967,3968],{},"至少要有两个同步副本可用，写入才允许成功。",[18,3970,3971],{},"如果 ISR 只剩一个副本，Broker 会拒绝写入。此时系统牺牲部分可用性，避免在单副本状态下继续写入并承担更高的数据丢失风险。",[196,3973,3975],{"id":3974},"_4-关闭非同步副本选举","4. 关闭非同步副本选举",[204,3977,3979],{"className":3726,"code":3978,"language":3728,"meta":210,"style":210},"unclean.leader.election.enable=false\n",[119,3980,3981],{"__ignoreMap":210},[460,3982,3983],{"class":462,"line":463},[460,3984,3978],{},[18,3986,3987,3990],{},[119,3988,3989],{},"unclean leader election"," 指允许不在 ISR 中的副本被选举为 Leader。如果所有同步副本都不可用，非 ISR 副本可能缺少最新消息；将它选为 Leader 虽然能更快恢复分区服务，却可能造成日志回退和数据丢失。",[18,3992,3993],{},"因此，对数据可靠性要求较高的场景应明确配置：",[57,3995,3996],{},[18,3997,3998],{},[28,3999,4000,4001,326],{},"关闭非同步副本选举：",[119,4002,3588],{},[18,4004,4005],{},"这是一个典型的取舍：",[204,4007,4010],{"className":4008,"code":4009,"language":209,"meta":210},[207],"开启非同步副本选举：可用性更高，但可能丢失消息\n关闭非同步副本选举：数据可靠性更强，但分区可能暂时不可用\n",[119,4011,4009],{"__ignoreMap":210},[196,4013,4015,4016,4018,4019],{"id":4014},"_5-acksall-不等于每条消息都立即-fsync","5. ",[119,4017,3580],{}," 不等于每条消息都立即 ",[119,4020,4021],{},"fsync",[18,4023,4024,4025,326],{},"Kafka 的持久性主要依赖顺序写日志、操作系统页缓存和多副本机制。Broker 返回成功，并不表示每个副本都对该消息单独执行了一次物理磁盘 ",[119,4026,4021],{},[18,4028,4029],{},"实际生产中，通常通过跨 Broker 副本降低单机和单盘故障风险，而不是强制每条消息同步刷盘。若多个副本同时发生不可恢复故障，仍不存在数学意义上的绝对零丢失。",[14,4031,4033],{"id":4032},"四消费者如何避免消费丢失","四、消费者如何避免“消费丢失”",[196,4035,4037],{"id":4036},"_1-关闭自动提交-offset","1. 关闭自动提交 Offset",[204,4039,4041],{"className":3726,"code":4040,"language":3728,"meta":210,"style":210},"enable.auto.commit=false\n",[119,4042,4043],{"__ignoreMap":210},[460,4044,4045],{"class":462,"line":463},[460,4046,4040],{},[18,4048,4049],{},"自动提交可能出现以下顺序：",[204,4051,4054],{"className":4052,"code":4053,"language":209,"meta":210},[207],"拉取消息\n→ 自动提交 Offset\n→ 执行业务\n→ 进程崩溃\n",[119,4055,4053],{"__ignoreMap":210},[18,4057,4058],{},"重启后，消费者会从已提交的下一个 Offset 继续消费，刚才尚未处理成功的消息就被跳过了。",[196,4060,4062],{"id":4061},"_2-业务成功后再提交-offset","2. 业务成功后再提交 Offset",[18,4064,4065],{},"更可靠的顺序是：",[204,4067,4070],{"className":4068,"code":4069,"language":209,"meta":210},[207],"拉取消息\n→ 执行业务\n→ 业务成功\n→ 提交 Offset\n",[119,4071,4069],{"__ignoreMap":210},[18,4073,4074],{},"如果业务处理失败，就不提交 Offset，让消息后续重新消费：",[204,4076,4078],{"className":454,"code":4077,"language":456,"meta":210,"style":210},"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",[119,4079,4080,4085,4090,4094,4099,4104,4108,4112,4117],{"__ignoreMap":210},[460,4081,4082],{"class":462,"line":463},[460,4083,4084],{},"while (true) {\n",[460,4086,4087],{"class":462,"line":469},[460,4088,4089],{},"    ConsumerRecords\u003CString, String> records = consumer.poll(Duration.ofSeconds(1));\n",[460,4091,4092],{"class":462,"line":1158},[460,4093,2400],{"emptyLinePlaceholder":1211},[460,4095,4096],{"class":462,"line":2373},[460,4097,4098],{},"    for (ConsumerRecord\u003CString, String> record : records) {\n",[460,4100,4101],{"class":462,"line":2379},[460,4102,4103],{},"        process(record);\n",[460,4105,4106],{"class":462,"line":2417},[460,4107,3852],{},[460,4109,4110],{"class":462,"line":2423},[460,4111,2400],{"emptyLinePlaceholder":1211},[460,4113,4114],{"class":462,"line":2429},[460,4115,4116],{},"    consumer.commitSync();\n",[460,4118,4119],{"class":462,"line":2434},[460,4120,2426],{},[18,4122,4123],{},"示例表达的是基本原则。实际批量消费时，如果一批消息中只有部分成功，需要准确管理各分区可提交的 Offset，不能直接把失败消息之后的位置一起提交。",[196,4125,4127],{"id":4126},"_3-为什么还需要业务幂等","3. 为什么还需要业务幂等",[18,4129,4130],{},"“处理成功后提交”可以避免消息被跳过，但会引入重复：",[204,4132,4135],{"className":4133,"code":4134,"language":209,"meta":210},[207],"业务处理成功\n→ 提交 Offset 前进程崩溃\n→ 重启后再次消费同一条消息\n",[119,4136,4134],{"__ignoreMap":210},[18,4138,4139],{},"因此，可靠消费通常采用至少一次投递，并在业务层保证幂等。常见方式包括：",[174,4141,4142,4145,4148,4151,4154],{},[25,4143,4144],{},"使用事件 ID 建立数据库唯一索引。",[25,4146,4147],{},"建立消费去重表或 Inbox 表。",[25,4149,4150],{},"使用业务状态机，只允许合法状态转换。",[25,4152,4153],{},"更新时带版本号或业务条件。",[25,4155,4156],{},"对扣款、发券等操作使用唯一业务流水号。",[57,4158,4159],{},[18,4160,4161],{},[28,4162,4163],{},"不丢消息通常意味着允许重复，再通过幂等消除重复影响。",[196,4165,4167],{"id":4166},"_4-kafka-内部链路的-exactly-once","4. Kafka 内部链路的 Exactly Once",[18,4169,4170],{},"如果消费 Kafka 后仍然写回 Kafka，可以使用 Kafka 事务，把输出记录和消费 Offset 放进同一个事务：",[204,4172,4175],{"className":4173,"code":4174,"language":209,"meta":210},[207],"消费消息\n→ 处理\n→ 写入下游 Topic\n→ sendOffsetsToTransaction\n→ 提交事务\n",[119,4176,4174],{"__ignoreMap":210},[18,4178,4179],{},"下游消费者设置：",[204,4181,4183],{"className":3726,"code":4182,"language":3728,"meta":210,"style":210},"isolation.level=read_committed\n",[119,4184,4185],{"__ignoreMap":210},[460,4186,4187],{"class":462,"line":463},[460,4188,4182],{},[18,4190,4191],{},"这样只读取已提交事务的数据。",[18,4193,4194],{},"但如果最终写入的是 MySQL 等外部系统，Kafka 事务并不能自动保证 Kafka 与数据库的原子性。生产方可以用 Outbox 保证事件可靠发出，消费方可以用 Inbox 应对重复消费，具体区别见下一节。",[14,4196,4198],{"id":4197},"五outbox-与-inbox-分别是什么","五、Outbox 与 Inbox 分别是什么",[18,4200,4201],{},"Outbox 和 Inbox 都借助数据库本地事务解决消息链路中的一致性问题，但它们位于不同位置：",[204,4203,4206],{"className":4204,"code":4205,"language":209,"meta":210},[207],"业务生产方 → Outbox → Kafka → Inbox → 业务消费方\n",[119,4207,4205],{"__ignoreMap":210},[196,4209,4211],{"id":4210},"_1-outbox保证业务事件可靠发出","1. Outbox：保证业务事件可靠发出",[18,4213,4214],{},"Outbox 位于消息生产方，用来解决“数据库更新成功，但 Kafka 消息发送失败”的双写不一致问题。",[18,4216,4217],{},"核心流程：",[204,4219,4222],{"className":4220,"code":4221,"language":209,"meta":210},[207],"同一个数据库事务\n├── 更新业务数据\n└── 写入 Outbox 事件表\n\n事务提交\n→ 后台投递程序扫描 Outbox\n→ 发送 Kafka\n→ 收到成功确认后标记已发送\n",[119,4223,4221],{"__ignoreMap":210},[18,4225,4226],{},"例如创建订单时，不直接把“订单创建事件”的可靠性寄托在一次 Kafka 调用上，而是在创建订单的同一个数据库事务中写入一条 Outbox 记录。即使 Kafka 暂时不可用，后台任务仍可继续重试。",[18,4228,4229],{},"Outbox 的关键点：",[174,4231,4232,4235,4241,4244,4247],{},[25,4233,4234],{},"业务数据和事件记录必须写入同一个数据库事务。",[25,4236,4237,4238,326],{},"每条事件应有全局唯一的 ",[119,4239,4240],{},"eventId",[25,4242,4243],{},"投递程序可以轮询事件表，也可以通过 CDC 捕获新增事件。",[25,4245,4246],{},"“发送成功但标记失败”可能导致再次投递，因此消费者仍需幂等。",[25,4248,4249],{},"Outbox 解决的是事件可靠发出，不保证消费者一定处理成功。",[18,4251,4252,4253,4256],{},"这里的 CDC 是 ",[28,4254,4255],{},"Change Data Capture（变更数据捕获）","。它会读取数据库的变更日志，例如 MySQL Binlog，捕获数据的新增、修改和删除，再把变更发送到 Kafka。常用工具有 Debezium、Canal。",[18,4258,4259],{},"在 Outbox 场景中，它的工作链路是：",[204,4261,4264],{"className":4262,"code":4263,"language":209,"meta":210},[207],"业务事务写入业务表和 Outbox 表\n→ CDC 监听 Binlog\n→ 捕获新增的 Outbox 记录\n→ 发送到 Kafka\n",[119,4265,4263],{"__ignoreMap":210},[18,4267,4268,4269,4271],{},"相比定时扫描 Outbox 表，CDC 延迟更低，也能减少频繁查询数据库的压力。但消息链路仍可能产生重复，因此必须保留 ",[119,4270,4240],{},"，并由消费端保证幂等。",[196,4273,4275],{"id":4274},"_2-inbox保证重复消息只产生一次业务效果","2. Inbox：保证重复消息只产生一次业务效果",[18,4277,4278],{},"Inbox 位于消息消费方，用来记录已经处理过的事件，主要解决至少一次投递带来的重复消费问题。",[18,4280,4217],{},[204,4282,4285],{"className":4283,"code":4284,"language":209,"meta":210},[207],"收到 Kafka 消息\n→ 开启数据库事务\n→ 根据 eventId 写入 Inbox 记录\n→ 执行业务更新\n→ 提交数据库事务\n→ 提交 Kafka Offset\n",[119,4286,4284],{"__ignoreMap":210},[18,4288,4289,4290,4292],{},"Inbox 表通常对 ",[119,4291,4240],{}," 建立唯一索引。重复消息到达时，如果发现该事件已经存在，就不再重复执行业务逻辑。",[18,4294,4295],{},"Inbox 的关键点：",[174,4297,4298,4301,4304,4307],{},[25,4299,4300],{},"Inbox 记录和业务更新必须处于同一个数据库事务。",[25,4302,4303],{},"唯一索引负责防止同一事件被并发重复处理。",[25,4305,4306],{},"数据库事务提交后、Offset 提交前发生故障，消息仍会重投，但 Inbox 可以识别重复。",[25,4308,4309],{},"如果消费逻辑还会调用外部 HTTP 接口，Inbox 无法自动让数据库和外部接口形成原子事务；仍需下游幂等、重试或补偿。",[196,4311,4313],{"id":4312},"_3-两者的区别","3. 两者的区别",[92,4315,4316,4328],{},[95,4317,4318],{},[98,4319,4320,4322,4325],{},[101,4321,2756],{},[101,4323,4324],{},"Outbox",[101,4326,4327],{},"Inbox",[111,4329,4330,4341,4352,4363,4374],{},[98,4331,4332,4335,4338],{},[116,4333,4334],{},"所在位置",[116,4336,4337],{},"消息生产方",[116,4339,4340],{},"消息消费方",[98,4342,4343,4346,4349],{},[116,4344,4345],{},"主要目标",[116,4347,4348],{},"防止业务成功但事件未发出",[116,4350,4351],{},"防止重复消息重复产生业务效果",[98,4353,4354,4357,4360],{},[116,4355,4356],{},"本地事务内容",[116,4358,4359],{},"业务更新 + 写事件表",[116,4361,4362],{},"写消费记录 + 业务更新",[98,4364,4365,4368,4371],{},[116,4366,4367],{},"后台动作",[116,4369,4370],{},"把待发送事件投递到 Kafka",[116,4372,4373],{},"通常不需要再次投递",[98,4375,4376,4379,4382],{},[116,4377,4378],{},"仍需注意",[116,4380,4381],{},"投递过程可能产生重复消息",[116,4383,4384],{},"外部调用仍需幂等或补偿",[18,4386,4387],{},"一句话区分：",[57,4389,4390],{},[18,4391,4392],{},[28,4393,4394],{},"Outbox 保证“该发的消息最终发出去”，Inbox 保证“重复收到的消息不会重复生效”。",[14,4396,4398],{"id":4397},"六三种投递语义","六、三种投递语义",[92,4400,4401,4414],{},[95,4402,4403],{},[98,4404,4405,4408,4411],{},[101,4406,4407],{},"语义",[101,4409,4410],{},"Offset 与业务处理顺序",[101,4412,4413],{},"结果",[111,4415,4416,4427,4438],{},[98,4417,4418,4421,4424],{},[116,4419,4420],{},"At Most Once",[116,4422,4423],{},"先提交，再处理",[116,4425,4426],{},"可能丢失，通常不重复",[98,4428,4429,4432,4435],{},[116,4430,4431],{},"At Least Once",[116,4433,4434],{},"先处理，再提交",[116,4436,4437],{},"不轻易丢失，可能重复",[98,4439,4440,4443,4446],{},[116,4441,4442],{},"Exactly Once",[116,4444,4445],{},"事务或幂等机制协调",[116,4447,4448],{},"业务效果恰好一次",[18,4450,4451],{},"绝大多数业务系统采用：",[57,4453,4454],{},[18,4455,4456],{},[28,4457,4458],{},"Kafka 至少一次投递 + 消费端业务幂等。",[18,4460,4461],{},"这通常比追求所有环节的强事务更容易实现，也更便于扩展。",[14,4463,4465],{"id":4464},"七常见故障与保障措施","七、常见故障与保障措施",[92,4467,4468,4481],{},[95,4469,4470],{},[98,4471,4472,4475,4478],{},[101,4473,4474],{},"故障场景",[101,4476,4477],{},"可能后果",[101,4479,4480],{},"主要保障",[111,4482,4483,4494,4507,4520,4532,4543,4554,4565],{},[98,4484,4485,4488,4491],{},[116,4486,4487],{},"生产者网络抖动",[116,4489,4490],{},"发送失败或结果未知",[116,4492,4493],{},"重试、幂等生产、回调处理",[98,4495,4496,4499,4502],{},[116,4497,4498],{},"Leader 写入后立即故障",[116,4500,4501],{},"未同步消息丢失",[116,4503,4504,4506],{},[119,4505,3580],{},"、副本、ISR",[98,4508,4509,4512,4515],{},[116,4510,4511],{},"ISR 只剩一个副本",[116,4513,4514],{},"单点故障风险升高",[116,4516,4517,4519],{},[119,4518,3742],{}," 拒绝写入",[98,4521,4522,4525,4528],{},[116,4523,4524],{},"同步副本全部不可用",[116,4526,4527],{},"非同步副本选主后日志回退",[116,4529,4000,4530],{},[119,4531,3588],{},[98,4533,4534,4537,4540],{},[116,4535,4536],{},"消费者先提交后处理",[116,4538,4539],{},"业务消息被跳过",[116,4541,4542],{},"关闭自动提交，成功后提交",[98,4544,4545,4548,4551],{},[116,4546,4547],{},"处理成功但提交前崩溃",[116,4549,4550],{},"重复消费",[116,4552,4553],{},"业务幂等、唯一流水号",[98,4555,4556,4559,4562],{},[116,4557,4558],{},"数据库成功、Kafka 失败",[116,4560,4561],{},"业务事件未发出",[116,4563,4564],{},"Transactional Outbox",[98,4566,4567,4570,4573],{},[116,4568,4569],{},"Kafka 成功、下游失败",[116,4571,4572],{},"业务结果缺失",[116,4574,4575],{},"重试、死信、补偿、告警",[14,4577,4579],{"id":4578},"八常见误区","八、常见误区",[196,4581,4583,4584,4586],{"id":4582},"误区一配置-acksall-就绝对不会丢","误区一：配置 ",[119,4585,3580],{}," 就绝对不会丢",[18,4588,4589,4591,4592,4594],{},[119,4590,3580],{}," 只解决生产者到 Broker 的确认强度，还要配合副本数、",[119,4593,3742],{},"、正确选主和消费者提交策略。",[196,4596,4598],{"id":4597},"误区二配置无限重试就不会丢","误区二：配置无限重试就不会丢",[18,4600,4601,4602,4604],{},"重试仍然受 ",[119,4603,3795],{}," 等条件限制，而且权限错误、序列化错误等问题无法靠盲目重试解决。最终失败必须被业务感知和补偿。",[196,4606,4608],{"id":4607},"误区三手动提交-offset-就是-exactly-once","误区三：手动提交 Offset 就是 Exactly Once",[18,4610,4611],{},"手动提交只能帮助建立“处理成功后提交”的顺序，崩溃窗口仍可能造成重复消费，因此还需要业务幂等。",[196,4613,4615],{"id":4614},"误区四kafka-事务可以自动覆盖数据库","误区四：Kafka 事务可以自动覆盖数据库",[18,4617,4618],{},"Kafka 事务主要协调 Kafka 内部的消息与 Offset，不会自动与 MySQL 等外部数据库形成同一个原子事务。",[196,4620,4622],{"id":4621},"误区五broker-返回成功就等于所有副本已经物理刷盘","误区五：Broker 返回成功就等于所有副本已经物理刷盘",[18,4624,4625],{},"Kafka 的确认、副本复制和磁盘刷盘是不同概念。高可靠主要依靠多副本和同步副本约束，而不是把每条消息都单独同步刷盘。",[14,4627,4629],{"id":4628},"九监控和运维同样重要","九、监控和运维同样重要",[18,4631,4632],{},"配置正确后，还应持续监控：",[174,4634,4635,4638,4641,4644,4647,4650],{},[25,4636,4637],{},"发送失败率、重试次数和发送延迟。",[25,4639,4640],{},"ISR 收缩、未充分复制分区和离线分区。",[25,4642,4643],{},"Broker 磁盘空间、磁盘延迟和网络异常。",[25,4645,4646],{},"消费积压、消费失败、重平衡次数。",[25,4648,4649],{},"死信消息和补偿任务堆积。",[25,4651,4652],{},"业务事件与最终结果的对账差异。",[18,4654,4655],{},"没有监控和对账，即使消息已经丢失，系统也可能长期无法发现。",[14,4657,1092],{"id":1092},[174,4659,4660,4667,4674,4681,4688],{},[25,4661,4662],{},[70,4663,4666],{"href":4664,"rel":4665},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fconfiguration\u002Fproducer-configs\u002F",[1101],"Apache Kafka Producer Configs",[25,4668,4669],{},[70,4670,4673],{"href":4671,"rel":4672},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fconfiguration\u002Fbroker-configs\u002F",[1101],"Apache Kafka Broker Configs",[25,4675,4676],{},[70,4677,4680],{"href":4678,"rel":4679},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fconfiguration\u002Fconsumer-configs\u002F",[1101],"Apache Kafka Consumer Configs",[25,4682,4683],{},[70,4684,4687],{"href":4685,"rel":4686},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fdesign\u002Fdesign\u002F",[1101],"Apache Kafka Design：Message Delivery Semantics",[25,4689,4690],{},[70,4691,4694],{"href":4692,"rel":4693},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fjavadoc\u002Forg\u002Fapache\u002Fkafka\u002Fclients\u002Fconsumer\u002FKafkaConsumer.html",[1101],"Apache KafkaConsumer JavaDoc",[1146,4696,1148],{},{"title":210,"searchDepth":469,"depth":469,"links":4698},[4699,4700,4701,4702,4703,4710,4719,4725,4730,4731,4732,4740,4741],{"id":16,"depth":469,"text":16},{"id":66,"depth":469,"text":66},{"id":83,"depth":469,"text":83},{"id":3627,"depth":469,"text":3628},{"id":3651,"depth":469,"text":3652,"children":4704},[4705,4707,4708,4709],{"id":3655,"depth":1158,"text":4706},"1. 使用 acks=all",{"id":3746,"depth":1158,"text":3747},{"id":3802,"depth":1158,"text":3803},{"id":3867,"depth":1158,"text":3868},{"id":3895,"depth":469,"text":3896,"children":4711},[4712,4713,4714,4716,4717],{"id":3899,"depth":1158,"text":3900},{"id":3918,"depth":1158,"text":3919},{"id":3948,"depth":1158,"text":4715},"3. 配置 min.insync.replicas",{"id":3974,"depth":1158,"text":3975},{"id":4014,"depth":1158,"text":4718},"5. acks=all 不等于每条消息都立即 fsync",{"id":4032,"depth":469,"text":4033,"children":4720},[4721,4722,4723,4724],{"id":4036,"depth":1158,"text":4037},{"id":4061,"depth":1158,"text":4062},{"id":4126,"depth":1158,"text":4127},{"id":4166,"depth":1158,"text":4167},{"id":4197,"depth":469,"text":4198,"children":4726},[4727,4728,4729],{"id":4210,"depth":1158,"text":4211},{"id":4274,"depth":1158,"text":4275},{"id":4312,"depth":1158,"text":4313},{"id":4397,"depth":469,"text":4398},{"id":4464,"depth":469,"text":4465},{"id":4578,"depth":469,"text":4579,"children":4733},[4734,4736,4737,4738,4739],{"id":4582,"depth":1158,"text":4735},"误区一：配置 acks=all 就绝对不会丢",{"id":4597,"depth":1158,"text":4598},{"id":4607,"depth":1158,"text":4608},{"id":4614,"depth":1158,"text":4615},{"id":4621,"depth":1158,"text":4622},{"id":4628,"depth":469,"text":4629},{"id":1092,"depth":469,"text":1092},"wiki:java:kafka-message-loss-prevention","从生产者、Broker 和消费者三段链路分析 Kafka 消息丢失的原因，以及 acks、ISR、副本、手动提交 Offset 和业务幂等的完整保障方案。",{},72,"\u002Fjava\u002Fkafka-message-loss-prevention",{"title":3565,"description":4743},"java\u002Fkafka-message-loss-prevention","2026-07-24","3f0HtflG-t8Ejs1I4PaPFqW8UqAizA0ZOhF0kJToSBs",{"id":4752,"title":4753,"body":4754,"commentId":5758,"description":5759,"difficulty":2636,"draft":1208,"extension":1209,"meta":5760,"navigation":1211,"order":5761,"path":5762,"section":1214,"seo":5763,"stem":5764,"updated":4749,"__hash__":5765},"java\u002Fjava\u002Fkafka-high-concurrency-performance-availability.md","Kafka 是如何实现高并发、高性能、高可用的",{"type":7,"value":4755,"toc":5714},[4756,4759,4761,4775,4777,4784,4786,4795,4797,4800,4814,4818,4822,4825,4831,4834,4841,4845,4848,4851,4862,4865,4869,4872,4874,4880,4883,4887,4890,4893,4897,4901,4904,4910,4913,4917,4920,4926,4946,4949,4953,4956,4962,4965,4979,4985,4989,4992,4998,5001,5018,5025,5031,5034,5037,5044,5047,5067,5076,5080,5083,5086,5100,5116,5120,5123,5129,5135,5141,5144,5153,5159,5162,5169,5175,5179,5182,5185,5191,5193,5199,5202,5208,5211,5214,5217,5232,5235,5239,5243,5246,5252,5255,5258,5262,5265,5271,5274,5283,5286,5291,5294,5297,5301,5305,5316,5319,5333,5336,5339,5345,5348,5352,5359,5365,5368,5371,5419,5422,5428,5431,5435,5438,5441,5447,5450,5454,5516,5519,5525,5529,5532,5552,5555,5558,5564,5567,5571,5575,5578,5582,5588,5592,5595,5599,5602,5609,5616,5620,5623,5634,5638,5651,5654,5668,5670,5712],[10,4757,4753],{"id":4758},"kafka-是如何实现高并发高性能高可用的",[14,4760,16],{"id":16},[57,4762,4763],{},[18,4764,4765,4766,4769,4770,892,4772,4774],{},"Kafka 的高并发主要依靠分区实现横向扩展：一个 Topic 被拆成多个 Partition，分区 Leader 分散在不同 Broker 上，生产者可以并行写入，消费者组也可以按分区并行消费。高性能主要来自追加写和顺序 I\u002FO、操作系统 Page Cache、生产与消费两端的批处理、批量压缩，以及通过 ",[119,4767,4768],{},"sendfile"," 实现的零拷贝传输。高可用则依靠多副本、Leader\u002FFollower、ISR、故障选主和 KRaft Controller Quorum；生产端再配合 ",[119,4771,3580],{},[119,4773,3742],{}," 和幂等生产，消费端通过 Consumer Group、Offset 与重新分配实现故障恢复。Kafka 的核心设计是在分区粒度上并行，在单分区内部维持有序日志，再用副本机制保障故障后的连续服务。",[18,4776,55],{},[57,4778,4779],{},[18,4780,4781],{},[28,4782,4783],{},"分区提供并行度，顺序日志、批处理和零拷贝提供吞吐量，副本、ISR 与故障选主提供可用性。",[14,4785,66],{"id":66},[18,4787,4788],{},[70,4789,4792],{"href":4790,"target":73,"rel":4791},"\u002Fimages\u002Fwiki\u002Fjava\u002Fkafka-high-concurrency-performance-availability.svg",[75,76],[78,4793],{"src":4790,"alt":4794},"Kafka 高并发、高性能和高可用实现机制总览",[14,4796,83],{"id":83},[18,4798,4799],{},"Kafka 的三个能力不是彼此孤立的：",[174,4801,4802,4805,4808,4811],{},[25,4803,4804],{},"分区既是并发执行和横向扩展的单位，也是副本复制与故障恢复的单位。",[25,4806,4807],{},"批处理既减少网络请求，也把大量小写入转化为更高效的顺序写。",[25,4809,4810],{},"Page Cache 既提升读写性能，也能配合副本机制避免为了可靠性而对每条消息执行同步刷盘。",[25,4812,4813],{},"副本增强了可靠性，但同时会增加网络、磁盘与确认延迟，因此需要在吞吐、延迟和可靠性之间取舍。",[14,4815,4817],{"id":4816},"一高并发通过分区横向扩展","一、高并发：通过分区横向扩展",[196,4819,4821],{"id":4820},"_1-partition-是-kafka-的并行单元","1. Partition 是 Kafka 的并行单元",[18,4823,4824],{},"一个 Topic 可以拆成多个 Partition：",[204,4826,4829],{"className":4827,"code":4828,"language":209,"meta":210},[207],"Topic: order-events\n├── Partition 0 → Leader 在 Broker A\n├── Partition 1 → Leader 在 Broker B\n└── Partition 2 → Leader 在 Broker C\n",[119,4830,4828],{"__ignoreMap":210},[18,4832,4833],{},"不同分区可以由不同 Broker 同时处理，因此整体吞吐不再受限于单台机器。增加 Broker 并合理增加分区后，存储容量、网络带宽和读写能力都可以横向扩展。",[18,4835,4836,4837,4840],{},"Kafka 只保证",[28,4838,4839],{},"单分区内有序","，不保证一个 Topic 的所有分区之间全局有序。这是它获得并行能力的重要前提。",[196,4842,4844],{"id":4843},"_2-生产者直接找到分区-leader","2. 生产者直接找到分区 Leader",[18,4846,4847],{},"生产者会缓存集群元数据，根据消息 Key、显式分区或默认分区策略选择 Partition，然后直接向该分区的 Leader 发送数据，中间不需要额外的统一路由节点。",[18,4849,4850],{},"常见分区方式：",[174,4852,4853,4856,4859],{},[25,4854,4855],{},"指定 Key：相同 Key 通常进入同一分区，适合保证同一订单、用户或账户内的消息顺序。",[25,4857,4858],{},"不指定 Key：默认分区策略会在适当时机选择分区，并尽量形成较大的批次。",[25,4860,4861],{},"自定义分区器：按租户、地区或业务规则路由。",[18,4863,4864],{},"如果 Key 分布不均，大量消息集中到少数分区，就会出现热点分区。此时即使 Broker 很多，整体吞吐仍会被热点 Leader 限制。",[196,4866,4868],{"id":4867},"_3-consumer-group-按分区并行消费","3. Consumer Group 按分区并行消费",[18,4870,4871],{},"同一个 Consumer Group 中，一个 Partition 在同一时刻只分配给一个 Consumer；一个 Consumer 可以负责多个 Partition。",[18,4873,666],{},[204,4875,4878],{"className":4876,"code":4877,"language":209,"meta":210},[207],"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",[119,4879,4877],{"__ignoreMap":210},[18,4881,4882],{},"因此，一个消费组的有效并行度上限通常受 Partition 数量约束。简单增加 Consumer，而不增加 Partition，不一定能提高吞吐量。",[196,4884,4886],{"id":4885},"_4-broker-内部通过线程分工处理请求","4. Broker 内部通过线程分工处理请求",[18,4888,4889],{},"Broker 不会用一个线程串行完成所有网络和磁盘工作。网络线程负责接收请求、解析协议和返回响应，请求处理线程负责执行 Produce、Fetch 等具体逻辑，后台线程负责日志清理、副本同步等任务。",[18,4891,4892],{},"这种职责分离让网络连接管理、业务请求处理和后台维护能够并行执行，避免某类工作完全阻塞其他工作。",[14,4894,4896],{"id":4895},"二高性能把随机小操作变成顺序批量操作","二、高性能：把随机小操作变成顺序批量操作",[196,4898,4900],{"id":4899},"_1-顺序追加写","1. 顺序追加写",[18,4902,4903],{},"Kafka 的 Partition 本质上是一条只追加的日志。新消息写到日志末尾，不需要像通用数据库那样频繁进行随机位置更新。",[204,4905,4908],{"className":4906,"code":4907,"language":209,"meta":210},[207],"旧数据 ────────────────→ 日志末尾追加新批次\noffset 0  1  2  3  4  5  6 ...\n",[119,4909,4907],{"__ignoreMap":210},[18,4911,4912],{},"顺序 I\u002FO 能充分利用磁盘和操作系统的预读、合并写能力。日志达到一定大小或时间后会切分为新的 Segment，旧 Segment 可以按保留策略清理或压缩，不影响当前日志继续追加。",[196,4914,4916],{"id":4915},"_2-分段日志与稀疏索引","2. 分段日志与稀疏索引",[18,4918,4919],{},"每个 Partition 由多个日志 Segment 组成，常见文件包括：",[204,4921,4924],{"className":4922,"code":4923,"language":209,"meta":210},[207],"00000000000000000000.log\n00000000000000000000.index\n00000000000000000000.timeindex\n",[119,4925,4923],{"__ignoreMap":210},[174,4927,4928,4934,4940],{},[25,4929,4930,4933],{},[119,4931,4932],{},".log"," 保存消息批次。",[25,4935,4936,4939],{},[119,4937,4938],{},".index"," 保存相对 Offset 到日志物理位置的稀疏映射。",[25,4941,4942,4945],{},[119,4943,4944],{},".timeindex"," 保存时间戳到相对 Offset 的稀疏映射。",[18,4947,4948],{},"查找消息时，Kafka 先定位 Segment，再通过稀疏索引找到接近目标的位置，最后在日志中继续顺序查找。索引不需要记录每一条消息，因此能控制索引体积和内存占用。",[196,4950,4952],{"id":4951},"_3-充分利用-page-cache","3. 充分利用 Page Cache",[18,4954,4955],{},"Kafka 不把全部消息封装成大量 Java 堆对象长期缓存，而是依赖操作系统 Page Cache：",[204,4957,4960],{"className":4958,"code":4959,"language":209,"meta":210},[207],"Producer\n→ Broker 写日志文件\n→ 数据先进入 Page Cache\n→ 操作系统异步回写磁盘\n",[119,4961,4959],{"__ignoreMap":210},[18,4963,4964],{},"这样做有几个好处：",[174,4966,4967,4970,4973,4976],{},[25,4968,4969],{},"利用操作系统成熟的预读和回写机制。",[25,4971,4972],{},"减少 JVM 堆内存占用和 GC 压力。",[25,4974,4975],{},"热数据可以直接从 Page Cache 读取。",[25,4977,4978],{},"Broker 重启后，操作系统缓存仍可能保留部分热数据，不必完全重新构建 JVM 内缓存。",[18,4980,4981,4982,4984],{},"Kafka 的可靠性主要依赖多副本，而不是对每条消息都执行一次同步 ",[119,4983,4021],{},"。如果强制每条消息同步刷盘，吞吐和延迟都会明显恶化。",[196,4986,4988],{"id":4987},"_4-端到端批处理","4. 端到端批处理",[18,4990,4991],{},"批处理贯穿生产、Broker 存储和消费三段链路：",[204,4993,4996],{"className":4994,"code":4995,"language":209,"meta":210},[207],"多条消息\n→ Producer 聚合成 Record Batch\n→ 一次网络请求发送\n→ Broker 将 Record Batch 连续追加到分区日志\n→ Consumer 一次 Fetch 多条消息\n",[119,4997,4995],{"__ignoreMap":210},[18,4999,5000],{},"它可以摊薄：",[174,5002,5003,5006,5009,5012,5015],{},[25,5004,5005],{},"网络往返成本。",[25,5007,5008],{},"系统调用成本。",[25,5010,5011],{},"协议头和请求头开销。",[25,5013,5014],{},"磁盘写入次数。",[25,5016,5017],{},"消息压缩成本。",[18,5019,5020,5021,5024],{},"这里的“Broker 追加批次”不是把多条消息合并成一条业务消息，也不承诺底层一定只发生一次系统调用。它表示 Kafka 的生产者和存储格式都以 ",[28,5022,5023],{},"Record Batch"," 为基本单位：",[204,5026,5029],{"className":5027,"code":5028,"language":209,"meta":210},[207],"Record Batch\n├── 批次头：baseOffset、长度、时间戳、压缩类型、CRC 等\n├── Record 1\n├── Record 2\n└── Record 3\n",[119,5030,5028],{"__ignoreMap":210},[18,5032,5033],{},"Producer 会先按 Partition 在内存中积累消息，形成 Record Batch。Broker 收到后，对批次进行校验，为其中的消息确定 Offset，然后把整段连续数据追加到对应 Partition 的当前日志 Segment 末尾。相比每条消息都单独发请求、单独写日志，批次追加可以用更少的请求和更大的连续 I\u002FO 摊薄固定开销。",[18,5035,5036],{},"一个 ProduceRequest 还可以携带多个 Partition 的 Record Batch，但 Broker 最终仍然分别追加到各个 Partition 的日志中。因此：",[57,5038,5039],{},[18,5040,5041],{},[28,5042,5043],{},"批次是网络传输和日志写入的基本单位，Partition 才是日志组织、顺序保证和副本复制的基本单位。",[18,5045,5046],{},"生产者常见相关参数：",[204,5048,5050],{"className":3726,"code":5049,"language":3728,"meta":210,"style":210},"batch.size=16384\nlinger.ms=5\ncompression.type=lz4\n",[119,5051,5052,5057,5062],{"__ignoreMap":210},[460,5053,5054],{"class":462,"line":463},[460,5055,5056],{},"batch.size=16384\n",[460,5058,5059],{"class":462,"line":469},[460,5060,5061],{},"linger.ms=5\n",[460,5063,5064],{"class":462,"line":1158},[460,5065,5066],{},"compression.type=lz4\n",[18,5068,5069,2084,5072,5075],{},[119,5070,5071],{},"batch.size",[119,5073,5074],{},"linger.ms"," 体现的是吞吐与延迟之间的取舍：适度等待可以积累更大的批次，但等待过长会增加低流量场景的发送延迟。参数值应根据消息大小、流量和延迟目标压测确定，不能机械照搬示例。",[196,5077,5079],{"id":5078},"_5-批量压缩","5. 批量压缩",[18,5081,5082],{},"Kafka 对一个 Record Batch 进行整体压缩，而不是分别压缩每条消息。相似消息中的字段名和公共内容可以获得更好的压缩率。",[18,5084,5085],{},"压缩后的批次会以压缩形式写入日志并传输给消费者，从而降低：",[174,5087,5088,5091,5094,5097],{},[25,5089,5090],{},"Broker 磁盘占用。",[25,5092,5093],{},"生产者到 Broker 的网络流量。",[25,5095,5096],{},"副本复制流量。",[25,5098,5099],{},"Broker 到消费者的网络流量。",[18,5101,5102,5103,892,5106,892,5109,964,5112,5115],{},"压缩会消耗 CPU，因此需要根据资源瓶颈选择 ",[119,5104,5105],{},"lz4",[119,5107,5108],{},"zstd",[119,5110,5111],{},"snappy",[119,5113,5114],{},"gzip"," 等算法。网络或磁盘是瓶颈时，压缩往往能换取更高吞吐。",[196,5117,5119],{"id":5118},"_6-零拷贝","6. 零拷贝",[18,5121,5122],{},"消费者拉取日志数据时，普通路径可能需要：",[204,5124,5127],{"className":5125,"code":5126,"language":209,"meta":210},[207],"磁盘 → Page Cache → 用户空间 → Socket Buffer → 网卡\n",[119,5128,5126],{"__ignoreMap":210},[18,5130,5131,5132,5134],{},"Kafka 在适用条件下利用 Linux ",[119,5133,4768],{},"，让数据从 Page Cache 直接进入网络发送路径，减少用户态和内核态之间的复制与上下文切换：",[204,5136,5139],{"className":5137,"code":5138,"language":209,"meta":210},[207],"Page Cache → Socket \u002F 网卡\n",[119,5140,5138],{"__ignoreMap":210},[18,5142,5143],{},"这就是常说的零拷贝。它主要优化 Broker 向消费者传输已有日志数据的过程。",[18,5145,5146],{},[70,5147,5150],{"href":5148,"target":73,"rel":5149},"\u002Fimages\u002Fwiki\u002Fjava\u002Fkafka-sendfile-vs-traditional-io.svg",[75,76],[78,5151],{"src":5148,"alt":5152},"Kafka sendfile 零拷贝与传统文件传输路径对比",[18,5154,5155,5156,5158],{},"传统路径需要先把数据从内核 Page Cache 复制到 Broker 的用户空间，再从用户空间写回内核 Socket Buffer；",[119,5157,4768],{}," 让内核直接组织 Page Cache 到网络 Socket 的传输，Broker 不再把消息内容完整搬入 JVM 用户空间。",[18,5160,5161],{},"所以“零拷贝”更准确的含义是：",[57,5163,5164],{},[18,5165,5166],{},[28,5167,5168],{},"减少或绕过 Page Cache 与用户空间之间不必要的数据复制，而不是数据在磁盘、内存和网卡之间一次都不移动。",[18,5170,5171,5172,5174],{},"需要注意，Kafka 官方文档明确说明，启用 SSL 时数据要经过用户态 TLS 处理，当前不会使用这条 ",[119,5173,4768],{}," 路径。因此不能笼统地说所有 Kafka 网络传输都一定是零拷贝。",[196,5176,5178],{"id":5177},"_7-consumer-pull-与长轮询","7. Consumer Pull 与长轮询",[18,5180,5181],{},"Kafka 由消费者主动 Pull 数据。消费者可以根据自己的处理能力决定拉取速度，处理不过来时，未消费消息会暂时保留在 Kafka 中，并表现为 Consumer Lag 增长，而不是由 Broker 持续 Push 直至压垮消费者。",[18,5183,5184],{},"Consumer Lag 表示消费者的消费进度落后于 Partition 最新位置的程度。监控消费组时，通常可以近似理解为：",[204,5186,5189],{"className":5187,"code":5188,"language":209,"meta":210},[207],"Consumer Lag\n= Partition Log End Offset\n- Consumer Group 已提交 Offset\n",[119,5190,5188],{"__ignoreMap":210},[18,5192,666],{},[204,5194,5197],{"className":5195,"code":5196,"language":209,"meta":210},[207],"Partition 下一条待写位置：10000\n消费组下一条待消费位置：9400\n\nLag = 10000 - 9400 = 600\n",[119,5198,5196],{"__ignoreMap":210},[18,5200,5201],{},"这表示当前大约还有 600 个 Offset 的数据尚未被该消费组处理。Lag 不是消息丢失，而是消费积压：",[204,5203,5206],{"className":5204,"code":5205,"language":209,"meta":210},[207],"生产速度 > 消费速度\n→ Lag 持续增大\n\n消费速度 > 生产速度\n→ Consumer 逐步追赶\n→ Lag 逐步减小\n",[119,5207,5205],{"__ignoreMap":210},[18,5209,5210],{},"Lag 不能只看某一时刻的绝对值，还要观察增长趋势、积压持续时间和业务允许的最大延迟。同样数量的 Lag，在每秒几条和每秒几十万条的 Topic 中代表的时间延迟完全不同。",[18,5212,5213],{},"如果 Lag 长期增长，可能是消费者处理变慢、下游故障、分区热点、Consumer 数量不足或频繁重平衡。若积压时间超过 Topic 的数据保留期限，旧消息可能已经被清理，消费者就不能再依靠 Kafka 把这部分数据完整追回。",[18,5215,5216],{},"Fetch 请求支持批量拉取和长轮询：",[204,5218,5220],{"className":3726,"code":5219,"language":3728,"meta":210,"style":210},"fetch.min.bytes=1\nfetch.max.wait.ms=500\n",[119,5221,5222,5227],{"__ignoreMap":210},[460,5223,5224],{"class":462,"line":463},[460,5225,5226],{},"fetch.min.bytes=1\n",[460,5228,5229],{"class":462,"line":469},[460,5230,5231],{},"fetch.max.wait.ms=500\n",[18,5233,5234],{},"Broker 暂时没有足够数据时，可以等待一段时间再响应，避免消费者无数据时频繁空轮询；流量较高时，又可以一次返回较大的批次。",[14,5236,5238],{"id":5237},"三高可用副本isr-与故障转移","三、高可用：副本、ISR 与故障转移",[196,5240,5242],{"id":5241},"_1-每个-partition-可以有多个副本","1. 每个 Partition 可以有多个副本",[18,5244,5245],{},"假设副本因子为 3：",[204,5247,5250],{"className":5248,"code":5249,"language":209,"meta":210},[207],"Partition 0\n├── Leader：Broker A\n├── Follower：Broker B\n└── Follower：Broker C\n",[119,5251,5249],{"__ignoreMap":210},[18,5253,5254],{},"生产和普通消费请求由 Leader 处理，Follower 分别从 Leader 拉取并复制日志。多个 Follower 的同步是彼此独立的，不存在先同步 B、再由 B 同步 C 的固定顺序。",[18,5256,5257],{},"副本应尽量分散在不同 Broker；具备机架感知配置时，还可以跨机架放置，降低单机或单机架故障带来的影响。",[196,5259,5261],{"id":5260},"_2-isr-维护可安全选主的副本集合","2. ISR 维护可安全选主的副本集合",[18,5263,5264],{},"ISR 是 In-Sync Replicas，即当前与 Leader 保持同步的副本集合。Follower 故障或长时间落后时会被移出 ISR，重新追上后可以再次加入。",[204,5266,5269],{"className":5267,"code":5268,"language":209,"meta":210},[207],"正常：ISR = [A, B, C]\nC 落后：ISR = [A, B]\n",[119,5270,5268],{"__ignoreMap":210},[18,5272,5273],{},"Leader 故障后，Controller 会从符合条件的副本中选择新的 Leader。以 ISR 为基础进行选主，可以避免把明显缺少最新日志的副本直接作为新的数据源。",[196,5275,5277,5278,5280,5281],{"id":5276},"_3-acks-与-mininsyncreplicas","3. ",[119,5279,3664],{}," 与 ",[119,5282,3742],{},[18,5284,5285],{},"常见的高可靠组合是：",[204,5287,5289],{"className":5288,"code":3958,"language":209,"meta":210},[207],[119,5290,3958],{"__ignoreMap":210},[18,5292,5293],{},"含义是生产者等待当前 ISR 的确认，并且 ISR 至少要有两个副本，否则 Broker 拒绝写入。",[18,5295,5296],{},"这个配置体现了 CAP 取舍：副本不足时拒绝写入，会降低部分可用性，但可以避免在只剩单副本时继续写入并承担更高的数据丢失风险。",[196,5298,5300],{"id":5299},"_4-kraft-controller-quorum-保证控制面可用","4. KRaft Controller Quorum 保证控制面可用",[354,5302,5304],{"id":5303},"kraft-controller-quorum-是什么","KRaft Controller Quorum 是什么",[18,5306,5307,5308,5311,5312,5315],{},"KRaft 是 Kafka 自己实现的元数据管理机制，用来替代早期依赖的 ZooKeeper。集群中会有若干个承担 ",[119,5309,5310],{},"controller"," 角色的节点，这些 Controller 组成一个基于 Raft 共识协议的 ",[28,5313,5314],{},"Controller Quorum","，即控制器仲裁组。",[18,5317,5318],{},"它们维护的是集群元数据，而不是普通 Topic 的业务消息：",[174,5320,5321,5324,5327,5330],{},[25,5322,5323],{},"Broker 注册、存活状态和节点信息。",[25,5325,5326],{},"Topic 与 Partition 的定义。",[25,5328,5329],{},"每个 Partition 的副本分配、Leader 和 ISR。",[25,5331,5332],{},"配置、配额以及其他集群级元数据。",[18,5334,5335],{},"这些变化会写入 KRaft 的集群元数据日志，并复制给 Quorum 中的其他 Controller。一次元数据变更获得多数 Controller 确认后才算提交，因此单个 Controller 故障不会导致已提交元数据丢失。",[18,5337,5338],{},"常见部署是 3 个或 5 个 Controller：",[204,5340,5343],{"className":5341,"code":5342,"language":209,"meta":210},[207],"3 个 Controller → 多数派为 2，可容忍 1 个故障\n5 个 Controller → 多数派为 3，可容忍 2 个故障\n",[119,5344,5342],{"__ignoreMap":210},[18,5346,5347],{},"如果存活 Controller 失去多数派，集群不能安全提交新的元数据变更。此时已有数据读写是否还能短暂继续，取决于具体操作和现有元数据状态，但创建 Topic、Partition Leader 调度等控制面操作会受到影响。",[354,5349,5351],{"id":5350},"active-controller-是什么","Active Controller 是什么",[18,5353,5354,5355,5358],{},"Controller Quorum 在同一时刻会选出一个 Leader，Kafka 的文档和运行语境中通常称它为 ",[28,5356,5357],{},"Active Controller","。其他 Controller 作为 Follower 复制元数据日志，随时准备接管。",[204,5360,5363],{"className":5361,"code":5362,"language":209,"meta":210},[207],"Controller A：Active Controller\n├── 接收和处理元数据变更\n├── 管理 Broker 注册与故障\n├── 触发 Partition Leader 选举\n└── 把变更写入元数据日志\n\nController B、C：Follower Controllers\n├── 复制元数据日志\n└── Active Controller 故障后参与重新选举\n",[119,5364,5362],{"__ignoreMap":210},[18,5366,5367],{},"Active Controller 不是所有业务消息的入口，也不负责替 Broker 保存普通 Topic 数据。生产者和消费者仍然直接与 Partition Leader 所在的 Broker 通信。",[18,5369,5370],{},"需要明确区分两个层面：",[92,5372,5373,5389],{},[95,5374,5375],{},[98,5376,5377,5380,5383,5386],{},[101,5378,5379],{},"层面",[101,5381,5382],{},"主要角色",[101,5384,5385],{},"保存什么",[101,5387,5388],{},"负责什么",[111,5390,5391,5405],{},[98,5392,5393,5396,5399,5402],{},[116,5394,5395],{},"数据面",[116,5397,5398],{},"Broker、Partition Leader\u002FFollower",[116,5400,5401],{},"普通 Topic 的业务消息",[116,5403,5404],{},"生产、消费、副本复制",[98,5406,5407,5410,5413,5416],{},[116,5408,5409],{},"控制面",[116,5411,5412],{},"KRaft Controller Quorum",[116,5414,5415],{},"集群元数据日志",[116,5417,5418],{},"节点管理、元数据变更、Leader 调度",[18,5420,5421],{},"例如 Broker A 突然故障：",[204,5423,5426],{"className":5424,"code":5425,"language":209,"meta":210},[207],"Active Controller 感知 Broker A 失联\n→ 找出受影响的 Partition\n→ 从符合条件的副本中选择新 Leader\n→ 将选举结果写入元数据日志\n→ 多数 Controller 确认提交\n→ 向相关 Broker 发布新元数据\n→ 客户端刷新元数据并访问新 Leader\n",[119,5427,5425],{"__ignoreMap":210},[18,5429,5430],{},"如果 Active Controller 自身故障，剩余 Controller 会通过 Raft 重新选出新的 Active Controller。新节点已经复制了已提交的元数据日志，因此可以继续管理集群。",[196,5432,5434],{"id":5433},"_5-consumer-group-自动接管故障消费者的分区","5. Consumer Group 自动接管故障消费者的分区",[18,5436,5437],{},"消费者通过心跳维持组成员身份。如果某个 Consumer 崩溃或超时，Consumer Group 会重新分配它原来负责的 Partition，让其他 Consumer 接管。",[18,5439,5440],{},"消费进度保存在已提交的 Offset 中，因此新 Consumer 可以从相应位置继续处理：",[204,5442,5445],{"className":5443,"code":5444,"language":209,"meta":210},[207],"Consumer A 故障\n→ 触发重新分配\n→ Partition 交给 Consumer B\n→ B 从已提交 Offset 继续消费\n",[119,5446,5444],{"__ignoreMap":210},[18,5448,5449],{},"重平衡期间可能产生短暂停顿，所以高可用不等于无感切换。合理设置会话、心跳、处理时长，并避免频繁扩缩容，有助于减少重平衡影响。",[14,5451,5453],{"id":5452},"四三种能力如何协同","四、三种能力如何协同",[92,5455,5456,5472],{},[95,5457,5458],{},[98,5459,5460,5463,5466,5469],{},[101,5461,5462],{},"目标",[101,5464,5465],{},"核心机制",[101,5467,5468],{},"解决的问题",[101,5470,5471],{},"主要代价",[111,5473,5474,5488,5502],{},[98,5475,5476,5479,5482,5485],{},[116,5477,5478],{},"高并发",[116,5480,5481],{},"多 Partition、多 Broker、Consumer Group",[116,5483,5484],{},"并行生产、存储和消费",[116,5486,5487],{},"分区与元数据管理成本",[98,5489,5490,5493,5496,5499],{},[116,5491,5492],{},"高性能",[116,5494,5495],{},"顺序写、Page Cache、批处理、压缩、零拷贝",[116,5497,5498],{},"降低 I\u002FO、复制和网络开销",[116,5500,5501],{},"批处理延迟与 CPU 消耗",[98,5503,5504,5507,5510,5513],{},[116,5505,5506],{},"高可用",[116,5508,5509],{},"多副本、ISR、Controller Quorum、故障转移",[116,5511,5512],{},"节点故障后继续提供服务",[116,5514,5515],{},"副本存储、网络与确认延迟",[18,5517,5518],{},"可以把整个链路理解为：",[204,5520,5523],{"className":5521,"code":5522,"language":209,"meta":210},[207],"消息按 Partition 分流\n→ Producer 批量发送\n→ Leader 顺序追加到日志\n→ Follower 并行复制\n→ 满足确认条件后返回\n→ Consumer Group 按 Partition 批量拉取\n",[119,5524,5522],{"__ignoreMap":210},[14,5526,5528],{"id":5527},"五为什么不能只靠增加分区提升性能","五、为什么不能只靠增加分区提升性能",[18,5530,5531],{},"分区是扩展能力的基础，但分区不是越多越好。分区数量增加会同时带来：",[174,5533,5534,5537,5540,5543,5546,5549],{},[25,5535,5536],{},"更多日志目录、Segment 和文件句柄。",[25,5538,5539],{},"更多副本复制任务。",[25,5541,5542],{},"更大的集群元数据。",[25,5544,5545],{},"更高的 Leader 选举和故障恢复成本。",[25,5547,5548],{},"Consumer Group 重平衡开销。",[25,5550,5551],{},"单 Key 顺序范围更难调整。",[18,5553,5554],{},"合理做法是根据目标吞吐量、单分区压测能力、消费者并行度、副本数和未来增长空间估算，而不是一次创建大量分区。",[18,5556,5557],{},"一个简化估算思路：",[204,5559,5562],{"className":5560,"code":5561,"language":209,"meta":210},[207],"分区数 ≥ max(\n  目标生产吞吐 \u002F 单分区生产吞吐,\n  目标消费吞吐 \u002F 单消费者单分区吞吐,\n  期望消费并行度\n)\n",[119,5563,5561],{"__ignoreMap":210},[18,5565,5566],{},"最终仍需要在接近生产环境的机器、网络、副本配置和消息大小下进行压测。",[14,5568,5570],{"id":5569},"六常见误区","六、常见误区",[196,5572,5574],{"id":5573},"误区一kafka-使用磁盘所以一定比内存消息队列慢","误区一：Kafka 使用磁盘，所以一定比内存消息队列慢",[18,5576,5577],{},"Kafka 采用顺序追加、Page Cache 和批处理，大量读写实际在操作系统缓存中完成。性能取决于访问模式，而不是简单由“使用磁盘”决定。",[196,5579,5581],{"id":5580},"误区二零拷贝让数据完全不发生复制","误区二：零拷贝让数据完全不发生复制",[18,5583,5584,5585,5587],{},"零拷贝是减少不必要的用户态复制，并非物理意义上的一次复制都没有；而且启用 SSL 时不会走 Kafka 文档描述的 ",[119,5586,4768],{}," 路径。",[196,5589,5591],{"id":5590},"误区三消费者越多消费速度一定越快","误区三：消费者越多，消费速度一定越快",[18,5593,5594],{},"同一消费组内，一个 Partition 同时只能交给一个 Consumer。Consumer 数超过 Partition 数后，多出的 Consumer 不会获得分区。",[196,5596,5598],{"id":5597},"误区四副本越多高可用越高且没有代价","误区四：副本越多，高可用越高且没有代价",[18,5600,5601],{},"更多副本会增加存储、网络复制和写确认成本。副本数应根据故障域和可靠性目标确定，常见三副本不代表所有场景都必须相同。",[196,5603,5605,5606,5608],{"id":5604},"误区五acksall-就代表所有配置副本都已写入","误区五：",[119,5607,3580],{}," 就代表所有配置副本都已写入",[18,5610,5611,5613,5614,326],{},[119,5612,3580],{}," 等待的是当前 ISR，而不是所有配置副本。要避免 ISR 只剩一个副本时仍然成功写入，需要配合 ",[119,5615,3742],{},[14,5617,5619],{"id":5618},"七排查性能和可用性问题时看什么","七、排查性能和可用性问题时看什么",[196,5621,5622],{"id":5622},"生产端",[174,5624,5625,5628,5631],{},[25,5626,5627],{},"发送吞吐、请求延迟、错误率和重试次数。",[25,5629,5630],{},"批次大小、压缩率和缓冲区等待。",[25,5632,5633],{},"消息 Key 是否造成分区流量倾斜。",[196,5635,5637],{"id":5636},"broker","Broker",[174,5639,5640,5643,5646,5648],{},[25,5641,5642],{},"各 Broker、Partition 和 Leader 的流量是否均衡。",[25,5644,5645],{},"磁盘利用率、磁盘延迟、网络带宽和请求队列。",[25,5647,4640],{},[25,5649,5650],{},"Page Cache 命中效果和系统是否发生 Swap。",[196,5652,5653],{"id":5653},"消费端",[174,5655,5656,5659,5662,5665],{},[25,5657,5658],{},"Consumer Lag 及其增长速度。",[25,5660,5661],{},"单条消息处理耗时和批次处理耗时。",[25,5663,5664],{},"Consumer 数量与 Partition 数量是否匹配。",[25,5666,5667],{},"重平衡次数、心跳超时和 Offset 提交失败。",[14,5669,1092],{"id":1092},[174,5671,5672,5678,5685,5692,5697,5702,5707],{},[25,5673,5674],{},[70,5675,5677],{"href":4685,"rel":5676},[1101],"Apache Kafka Design",[25,5679,5680],{},[70,5681,5684],{"href":5682,"rel":5683},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Foperations\u002Fkraft\u002F",[1101],"Apache Kafka KRaft",[25,5686,5687],{},[70,5688,5691],{"href":5689,"rel":5690},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Fimplementation\u002Fmessage-format\u002F",[1101],"Apache Kafka Message Format",[25,5693,5694],{},[70,5695,4666],{"href":4664,"rel":5696},[1101],[25,5698,5699],{},[70,5700,4680],{"href":4678,"rel":5701},[1101],[25,5703,5704],{},[70,5705,4673],{"href":4671,"rel":5706},[1101],[25,5708,5709],{},[70,5710,4694],{"href":4692,"rel":5711},[1101],[1146,5713,1148],{},{"title":210,"searchDepth":469,"depth":469,"links":5715},[5716,5717,5718,5719,5725,5734,5742,5743,5744,5752,5757],{"id":16,"depth":469,"text":16},{"id":66,"depth":469,"text":66},{"id":83,"depth":469,"text":83},{"id":4816,"depth":469,"text":4817,"children":5720},[5721,5722,5723,5724],{"id":4820,"depth":1158,"text":4821},{"id":4843,"depth":1158,"text":4844},{"id":4867,"depth":1158,"text":4868},{"id":4885,"depth":1158,"text":4886},{"id":4895,"depth":469,"text":4896,"children":5726},[5727,5728,5729,5730,5731,5732,5733],{"id":4899,"depth":1158,"text":4900},{"id":4915,"depth":1158,"text":4916},{"id":4951,"depth":1158,"text":4952},{"id":4987,"depth":1158,"text":4988},{"id":5078,"depth":1158,"text":5079},{"id":5118,"depth":1158,"text":5119},{"id":5177,"depth":1158,"text":5178},{"id":5237,"depth":469,"text":5238,"children":5735},[5736,5737,5738,5740,5741],{"id":5241,"depth":1158,"text":5242},{"id":5260,"depth":1158,"text":5261},{"id":5276,"depth":1158,"text":5739},"3. acks 与 min.insync.replicas",{"id":5299,"depth":1158,"text":5300},{"id":5433,"depth":1158,"text":5434},{"id":5452,"depth":469,"text":5453},{"id":5527,"depth":469,"text":5528},{"id":5569,"depth":469,"text":5570,"children":5745},[5746,5747,5748,5749,5750],{"id":5573,"depth":1158,"text":5574},{"id":5580,"depth":1158,"text":5581},{"id":5590,"depth":1158,"text":5591},{"id":5597,"depth":1158,"text":5598},{"id":5604,"depth":1158,"text":5751},"误区五：acks=all 就代表所有配置副本都已写入",{"id":5618,"depth":469,"text":5619,"children":5753},[5754,5755,5756],{"id":5622,"depth":1158,"text":5622},{"id":5636,"depth":1158,"text":5637},{"id":5653,"depth":1158,"text":5653},{"id":1092,"depth":469,"text":1092},"wiki:java:kafka-high-concurrency-performance-availability","从分区并行、顺序写、Page Cache、批处理、零拷贝、副本与 ISR 等机制，系统理解 Kafka 高吞吐与高可用架构。",{},73,"\u002Fjava\u002Fkafka-high-concurrency-performance-availability",{"title":4753,"description":5759},"java\u002Fkafka-high-concurrency-performance-availability","7w2j1CaPDOxbjhwQCuQIpbyFqqVZ88ZfOwjhHG_Yqbc",{"id":5767,"title":5768,"body":5769,"commentId":7102,"description":7103,"difficulty":2636,"draft":1208,"extension":1209,"meta":7104,"navigation":1211,"order":7105,"path":7106,"section":1214,"seo":7107,"stem":7108,"updated":4749,"__hash__":7109},"java\u002Fjava\u002Fkafka-message-backlog-handling.md","Kafka 消息积压如何处理",{"type":7,"value":5770,"toc":7056},[5771,5774,5776,5781,5783,5790,5792,5801,5803,5806,5812,5815,5819,5822,5825,5830,5832,5838,5841,5844,5915,5918,5921,5927,5930,5936,5939,5942,5946,5950,5956,5959,5963,5966,5989,5996,6000,6003,6023,6026,6032,6035,6039,6042,6045,6051,6054,6058,6061,6078,6081,6085,6089,6092,6098,6101,6105,6108,6122,6125,6128,6134,6137,6141,6144,6147,6150,6154,6158,6161,6164,6170,6173,6177,6180,6203,6205,6211,6216,6219,6225,6228,6231,6248,6251,6255,6258,6286,6289,6317,6321,6324,6330,6333,6339,6342,6352,6355,6361,6364,6372,6386,6395,6399,6412,6415,6421,6425,6428,6434,6437,6443,6446,6449,6452,6455,6458,6489,6492,6496,6500,6503,6506,6513,6516,6520,6523,6526,6532,6535,6541,6544,6547,6552,6555,6558,6562,6568,6571,6575,6581,6587,6590,6593,6607,6611,6614,6620,6623,6627,6630,6633,6639,6642,6662,6665,6669,6672,6678,6681,6695,6698,6702,6705,6711,6714,6720,6723,6729,6731,6737,6740,6746,6750,6753,6760,6763,6780,6783,6787,6790,6793,6853,6860,6913,6916,6919,6925,6931,6934,6937,6943,6946,6950,6953,6976,6980,6984,6987,6994,6997,7001,7004,7008,7015,7018,7022,7025,7027,7053],[10,5772,5768],{"id":5773},"kafka-消息积压如何处理",[14,5775,16],{"id":16},[57,5777,5778],{},[18,5779,5780],{},"Kafka 消息积压不能一上来就盲目增加消费者，首先要确认积压范围和原因：观察各 Partition 的 Consumer Lag、生产速率、消费速率、消费者存活状态、重平衡、处理耗时和下游依赖。如果消费实例异常或下游故障，应先恢复服务；如果生产速度持续大于消费速度，需要对非核心生产流量限流，同时扩容消费者，但同一消费组的有效并行度不会超过 Partition 数量。如果单条处理过慢，应通过批量写库、减少同步 RPC、优化慢 SQL 或使用受控的异步处理提高单实例吞吐；如果只有个别 Partition 积压，要检查消息 Key 是否造成热点；如果被异常消息反复阻塞，需要有限重试后进入死信或人工补偿。恢复时间可以用“积压量 ÷（消费速率－生产速率）”估算，只有消费能力大于生产速度，积压才会真正下降。Offset 跳到最新只能在业务明确同意丢弃旧消息时使用，不能作为常规解决方案。",[18,5782,55],{},[57,5784,5785],{},[18,5786,5787],{},[28,5788,5789],{},"先判断为什么消费速度落后，再从减少流入、恢复异常、提升单实例吞吐和增加有效并行度四个方向处理。",[14,5791,66],{"id":66},[18,5793,5794],{},[70,5795,5798],{"href":5796,"target":73,"rel":5797},"\u002Fimages\u002Fwiki\u002Fjava\u002Fkafka-message-backlog-handling.svg",[75,76],[78,5799],{"src":5796,"alt":5800},"Kafka 消息积压排查与处理流程",[14,5802,83],{"id":83},[18,5804,5805],{},"Kafka 消息积压的本质是：",[204,5807,5810],{"className":5808,"code":5809,"language":209,"meta":210},[207],"一段时间内的生产速度\n>\n一段时间内的有效消费速度\n",[119,5811,5809],{"__ignoreMap":210},[18,5813,5814],{},"积压不是单一故障类型，它可能由突发流量、消费者异常、下游变慢、分区热点或异常消息阻塞引起。不同原因的处理方式完全不同。",[14,5816,5818],{"id":5817},"一什么是-kafka-消息积压","一、什么是 Kafka 消息积压",[18,5820,5821],{},"每个 Partition 中的消息都有 Offset。生产者持续向日志末尾写入，消费组通过已提交 Offset 记录自己的处理进度。",[18,5823,5824],{},"监控消费组时，Consumer Lag 通常可以近似理解为：",[204,5826,5828],{"className":5827,"code":5188,"language":209,"meta":210},[207],[119,5829,5188],{"__ignoreMap":210},[18,5831,666],{},[204,5833,5836],{"className":5834,"code":5835,"language":209,"meta":210},[207],"Partition Log End Offset：120000\n消费组已提交 Offset：95000\n\nLag = 120000 - 95000 = 25000\n",[119,5837,5835],{"__ignoreMap":210},[18,5839,5840],{},"表示这个消费组在该 Partition 上大约落后 25000 个 Offset。",[18,5842,5843],{},"需要同时观察三类指标：",[92,5845,5846,5858],{},[95,5847,5848],{},[98,5849,5850,5853,5855],{},[101,5851,5852],{},"指标",[101,5854,1614],{},[101,5856,5857],{},"判断价值",[111,5859,5860,5871,5882,5893,5904],{},[98,5861,5862,5865,5868],{},[116,5863,5864],{},"当前 Lag",[116,5866,5867],{},"还落后多少 Offset",[116,5869,5870],{},"反映积压规模",[98,5872,5873,5876,5879],{},[116,5874,5875],{},"Lag 增长速度",[116,5877,5878],{},"单位时间新增多少积压",[116,5880,5881],{},"判断问题是否正在恶化",[98,5883,5884,5887,5890],{},[116,5885,5886],{},"最老未处理消息年龄",[116,5888,5889],{},"最老待处理消息的时间戳距现在多久",[116,5891,5892],{},"反映业务已经延迟多久",[98,5894,5895,5898,5901],{},[116,5896,5897],{},"积压字节量",[116,5899,5900],{},"待处理消息占用的总字节数",[116,5902,5903],{},"反映网络、磁盘和反序列化压力",[98,5905,5906,5909,5912],{},[116,5907,5908],{},"单条处理耗时",[116,5910,5911],{},"处理一条或一批消息实际需要多久",[116,5913,5914],{},"用于估算积压清空速度",[18,5916,5917],{},"只看 Lag 绝对值容易误判。例如同样积压 10 万条，在每秒消费 20 万条的系统里可能很快恢复，在每秒只消费 100 条的系统里则非常严重。",[18,5919,5920],{},"Lag 是 Offset 数量差，本身只表示“落后多少条记录”，不能直接等同于时间延迟。对于相同的 10 万 Lag，消费者处理速度越快，处理完现有积压所需的时间越短：",[204,5922,5925],{"className":5923,"code":5924,"language":209,"meta":210},[207],"处理现有积压所需时间\n≈ Lag ÷ 消费速度\n",[119,5926,5924],{"__ignoreMap":210},[18,5928,5929],{},"如果生产者仍在持续写入，要估算 Lag 整体降为 0 的时间，则应扣除生产速度：",[204,5931,5934],{"className":5932,"code":5933,"language":209,"meta":210},[207],"Lag 归零时间\n≈ Lag ÷（消费速度－生产速度）\n",[119,5935,5933],{"__ignoreMap":210},[18,5937,5938],{},"只有消费速度大于生产速度，Lag 才会持续下降。至于当前业务已经延迟多久，应直接查看最老未处理消息的时间戳，而不是只根据 Lag 推算。",[18,5940,5941],{},"消息大小影响的是积压字节量、网络传输和反序列化成本；单条处理复杂度影响的是消费速度。它们都很重要，但不能与 Lag 条数或消息年龄混为同一个指标。",[14,5943,5945],{"id":5944},"二先判断积压发生在哪里","二、先判断积压发生在哪里",[196,5947,5949],{"id":5948},"_1-是所有-partition-积压还是少数-partition-积压","1. 是所有 Partition 积压，还是少数 Partition 积压",[204,5951,5954],{"className":5952,"code":5953,"language":209,"meta":210},[207],"所有 Partition 的 Lag 都增长\n→ 更可能是整体消费能力不足、消费者异常或下游故障\n\n只有少数 Partition 的 Lag 很高\n→ 更可能是 Key 倾斜、热点业务或个别异常消息\n",[119,5955,5953],{"__ignoreMap":210},[18,5957,5958],{},"不能只看 Topic 的总 Lag。总数会掩盖单个热点 Partition，应同时查看每个 Partition 的 Lag、生产速率和消费速率。",[196,5960,5962],{"id":5961},"_2-消费者是否正常工作","2. 消费者是否正常工作",[18,5964,5965],{},"先检查：",[174,5967,5968,5971,5974,5977,5983,5986],{},[25,5969,5970],{},"消费实例是否存活，是否频繁重启。",[25,5972,5973],{},"Consumer Group 中是否存在空闲或反复加入、退出的成员。",[25,5975,5976],{},"是否频繁发生重平衡。",[25,5978,5979,5982],{},[119,5980,5981],{},"poll()"," 是否长时间没有被调用。",[25,5984,5985],{},"Offset 是否持续推进。",[25,5987,5988],{},"消费日志中是否存在反序列化、鉴权、提交 Offset 或业务异常。",[18,5990,5991,5992,5995],{},"如果业务处理时间超过 ",[119,5993,5994],{},"max.poll.interval.ms","，消费者会被认为没有正常推进消费，Partition 可能被重新分配。消费者不断处理、超时、重平衡，又可能造成更严重的积压。",[196,5997,5999],{"id":5998},"_3-单条消息处理是否变慢","3. 单条消息处理是否变慢",[18,6001,6002],{},"Kafka 消费本身往往不是最慢的部分，真正瓶颈通常在业务逻辑：",[174,6004,6005,6008,6011,6014,6017,6020],{},[25,6006,6007],{},"数据库慢 SQL、锁等待或连接池耗尽。",[25,6009,6010],{},"调用外部 RPC 超时。",[25,6012,6013],{},"每条消息单独写库，缺少批处理。",[25,6015,6016],{},"序列化、解压或大对象处理消耗 CPU。",[25,6018,6019],{},"消费逻辑加了粒度过大的锁。",[25,6021,6022],{},"下游 Elasticsearch、Redis 或第三方接口限流。",[18,6024,6025],{},"应把消费耗时拆分为：",[204,6027,6030],{"className":6028,"code":6029,"language":209,"meta":210},[207],"拉取耗时\n+ 反序列化耗时\n+ 业务计算耗时\n+ 数据库 \u002F RPC \u002F 下游写入耗时\n+ Offset 提交耗时\n",[119,6031,6029],{"__ignoreMap":210},[18,6033,6034],{},"只有找到主要耗时，优化才有方向。",[196,6036,6038],{"id":6037},"_4-是否被毒消息阻塞","4. 是否被“毒消息”阻塞",[18,6040,6041],{},"毒消息是指某条数据因为格式错误、业务状态异常或下游约束问题，每次消费都会失败。",[18,6043,6044],{},"如果代码采用无限重试：",[204,6046,6049],{"className":6047,"code":6048,"language":209,"meta":210},[207],"消费失败\n→ 立即重试\n→ 再次失败\n→ 一直占住当前 Partition\n",[119,6050,6048],{"__ignoreMap":210},[18,6052,6053],{},"该 Partition 后续所有消息都可能无法继续处理，表现为单分区 Lag 持续增大。",[196,6055,6057],{"id":6056},"_5-broker-或基础设施是否成为瓶颈","5. Broker 或基础设施是否成为瓶颈",[18,6059,6060],{},"还要检查：",[174,6062,6063,6066,6069,6072,6075],{},[25,6064,6065],{},"Broker 磁盘延迟、网络带宽和 CPU。",[25,6067,6068],{},"Fetch 请求延迟和错误率。",[25,6070,6071],{},"ISR 收缩、离线分区和副本同步异常。",[25,6073,6074],{},"消费者所在机器的 CPU、内存、GC 和网络。",[25,6076,6077],{},"是否发生跨机房访问或网络抖动。",[18,6079,6080],{},"如果 Broker 或网络已经饱和，单纯增加 Consumer 可能只会制造更多请求和竞争。",[14,6082,6084],{"id":6083},"三紧急处理先止住-lag-增长","三、紧急处理：先止住 Lag 增长",[196,6086,6088],{"id":6087},"_1-恢复异常消费者和下游","1. 恢复异常消费者和下游",[18,6090,6091],{},"如果积压源于实例宕机、数据库故障或下游超时，应优先恢复根因：",[204,6093,6096],{"className":6094,"code":6095,"language":209,"meta":210},[207],"恢复消费实例\n→ 恢复数据库 \u002F RPC \u002F 下游\n→ 确认 Offset 开始推进\n→ 再评估是否需要扩容\n",[119,6097,6095],{"__ignoreMap":210},[18,6099,6100],{},"下游仍不可用时强行增加消费者，只会放大连接数、重试和超时流量。",[196,6102,6104],{"id":6103},"_2-对非核心生产流量限流或降级","2. 对非核心生产流量限流或降级",[18,6106,6107],{},"如果生产速度仍在持续上升，而消费端已经满负荷，可以在业务允许时：",[174,6109,6110,6113,6116,6119],{},[25,6111,6112],{},"限制非核心事件的生产速率。",[25,6114,6115],{},"暂停可延迟的定时任务和批量导入。",[25,6117,6118],{},"合并低价值事件。",[25,6120,6121],{},"对非核心功能降级。",[18,6123,6124],{},"这不是最终解决方案，但能降低流入速度，给消费端争取恢复窗口。",[18,6126,6127],{},"这里的“合并低价值事件”，不是随意删除消息，而是对业务允许只保留最终状态或聚合结果的事件进行压缩。例如：",[204,6129,6132],{"className":6130,"code":6131,"language":209,"meta":210},[207],"同一个商品在 1 秒内连续产生 20 次缓存刷新通知\n→ 只保留一次刷新通知\n\n同一设备连续上报多次进度：10%、20%、30%\n→ 如果业务只关心最新进度，可以只发送 30%\n\n大量指标增量事件\n→ 在生产端按时间窗口汇总后，发送一条聚合结果\n",[119,6133,6131],{"__ignoreMap":210},[18,6135,6136],{},"合并前必须先定义清楚业务语义，包括按什么 Key 合并、合并时间窗口多长、保留最新值还是累计值，以及异常时如何补偿。订单状态流转、支付、账户流水、库存扣减、审计日志等每条事件都具有独立业务意义，不能使用这种方式合并。",[196,6138,6140],{"id":6139},"_3-确认消息保留时间是否足够","3. 确认消息保留时间是否足够",[18,6142,6143],{},"积压消息依赖 Kafka 的日志保留策略。如果预计追赶需要数小时或数天，应确认 Topic 的保留时间和磁盘空间是否足够。",[18,6145,6146],{},"如果最老未消费消息超过保留期限，旧日志可能被清理。此时即使消费者恢复，也无法从 Kafka 读取已经删除的数据。",[18,6148,6149],{},"紧急情况下可以评估临时延长保留时间，但必须同步评估 Broker 磁盘容量，避免积压尚未解决又触发磁盘告警。",[14,6151,6153],{"id":6152},"四提升消费能力","四、提升消费能力",[196,6155,6157],{"id":6156},"_1-增加-consumer-实例","1. 增加 Consumer 实例",[18,6159,6160],{},"增加同一 Consumer Group 中的实例，可以让更多 Partition 并行消费。",[18,6162,6163],{},"但有效并行度受 Partition 数量限制：",[204,6165,6168],{"className":6166,"code":6167,"language":209,"meta":210},[207],"8 个 Partition + 4 个 Consumer\n→ 可以继续扩容\n\n8 个 Partition + 8 个 Consumer\n→ 已接近分区级并行上限\n\n8 个 Partition + 12 个 Consumer\n→ 约 4 个 Consumer 无 Partition 可处理\n",[119,6169,6167],{"__ignoreMap":210},[18,6171,6172],{},"因此扩容前要同时查看当前 Partition 数量、消费者数量和分配情况。",[196,6174,6176],{"id":6175},"_2-优化单实例处理吞吐","2. 优化单实例处理吞吐",[18,6178,6179],{},"常见优化方式：",[174,6181,6182,6185,6188,6191,6194,6197,6200],{},[25,6183,6184],{},"把逐条写数据库改为批量写入。",[25,6186,6187],{},"合并相同业务 Key 的重复更新。",[25,6189,6190],{},"优化 SQL、索引和事务范围。",[25,6192,6193],{},"减少消费线程中的同步远程调用。",[25,6195,6196],{},"对可并行步骤使用有界线程池。",[25,6198,6199],{},"复用连接和客户端，避免每条消息重复创建。",[25,6201,6202],{},"降低无必要的日志和对象序列化开销。",[18,6204,666],{},[204,6206,6209],{"className":6207,"code":6208,"language":209,"meta":210},[207],"原方案：\n每条消息 → 一次数据库事务\n\n优化后：\n一批消息 → 分组校验 → 批量写入 → 提交对应 Offset\n",[119,6210,6208],{"__ignoreMap":210},[18,6212,6213,6214,326],{},"批量越大不一定越好。批次过大会增加内存占用、事务时间和失败重试成本，还可能让一次处理时间超过 ",[119,6215,5994],{},[18,6217,6218],{},"这里的“同步远程调用”，是指消费线程发送 RPC 或 HTTP 请求后，必须等待对方返回才能继续处理下一条消息：",[204,6220,6223],{"className":6221,"code":6222,"language":209,"meta":210},[207],"消费一条消息\n→ 调用远程库存服务\n→ 阻塞等待 50 ms\n→ 收到结果\n→ 再处理下一条消息\n",[119,6224,6222],{"__ignoreMap":210},[18,6226,6227],{},"即使本地计算只需要 1 ms，线程的大部分时间也消耗在网络等待上。如果每条消息都串行调用一次远程服务，单线程理论吞吐很容易被远程调用耗时限制。",[18,6229,6230],{},"“减少同步 RPC”可以从以下方向处理：",[174,6232,6233,6236,6239,6242,6245],{},[25,6234,6235],{},"把逐条调用改为批量接口，例如一次查询或提交 100 条。",[25,6237,6238],{},"对可复用且允许短时间不一致的数据使用本地缓存，减少重复查询。",[25,6240,6241],{},"把互不依赖的调用改成受控并发，但必须限制并发数，避免压垮下游。",[25,6243,6244],{},"将通知、埋点等非核心副作用发送到下游 Topic，由独立消费者异步执行。",[25,6246,6247],{},"合理设置超时、熔断和连接池，避免故障调用长期占住消费线程。",[18,6249,6250],{},"如果必须拿到远程结果才能保证当前消息处理正确，就不能为了吞吐简单删除该 RPC。此时更重要的是批量化、限制并发、幂等和下游容量治理。",[196,6252,6254],{"id":6253},"_3-谨慎调整消费参数","3. 谨慎调整消费参数",[18,6256,6257],{},"常见参数包括：",[204,6259,6261],{"className":3726,"code":6260,"language":3728,"meta":210,"style":210},"max.poll.records=500\nmax.poll.interval.ms=300000\nfetch.min.bytes=1\nfetch.max.wait.ms=500\nmax.partition.fetch.bytes=1048576\n",[119,6262,6263,6268,6273,6277,6281],{"__ignoreMap":210},[460,6264,6265],{"class":462,"line":463},[460,6266,6267],{},"max.poll.records=500\n",[460,6269,6270],{"class":462,"line":469},[460,6271,6272],{},"max.poll.interval.ms=300000\n",[460,6274,6275],{"class":462,"line":1158},[460,6276,5226],{},[460,6278,6279],{"class":462,"line":2373},[460,6280,5231],{},[460,6282,6283],{"class":462,"line":2379},[460,6284,6285],{},"max.partition.fetch.bytes=1048576\n",[18,6287,6288],{},"调整原则：",[174,6290,6291,6301,6308,6314],{},[25,6292,6293,6294,6297,6298,6300],{},"如果一批消息处理不完，应适当减小 ",[119,6295,6296],{},"max.poll.records","，或优化处理速度，而不是只把 ",[119,6299,5994],{}," 调得无限大。",[25,6302,6303,6304,6307],{},"增大 ",[119,6305,6306],{},"fetch.min.bytes"," 可以提高批量拉取效率，但会增加低流量时的等待延迟。",[25,6309,6310,6313],{},[119,6311,6312],{},"max.partition.fetch.bytes"," 过小可能限制大批次拉取，但调大后要评估客户端内存。",[25,6315,6316],{},"参数优化只能降低 Kafka 拉取开销，无法解决数据库或 RPC 本身处理缓慢。",[196,6318,6320],{"id":6319},"_4-异步处理必须有边界","4. 异步处理必须有边界",[18,6322,6323],{},"一种常见设计是由 Poll 线程负责拉取，再把消息交给工作线程池处理：",[204,6325,6328],{"className":6326,"code":6327,"language":209,"meta":210},[207],"Poll 线程\n→ 有界队列\n→ 工作线程池\n→ 处理完成\n→ 按 Partition 推进可安全提交的 Offset\n",[119,6329,6327],{"__ignoreMap":210},[18,6331,6332],{},"为什么必须使用有界队列？假设 Kafka 已经积压 100 万条，Poll 线程仍然高速拉取并塞入无界队列，而工作线程每秒只能处理 100 条，那么积压只是从 Kafka 的磁盘日志转移到了 JVM 堆内存。结果可能是：",[204,6334,6337],{"className":6335,"code":6336,"language":209,"meta":210},[207],"队列持续增长\n→ 堆内存占用上升\n→ Full GC 频繁\n→ 消费进程响应变慢甚至 OOM\n→ 未提交的内存任务随进程退出而丢失并被重新投递\n",[119,6338,6336],{"__ignoreMap":210},[18,6340,6341],{},"Kafka 本身就是持久化队列，没有必要把尚未具备处理能力的消息提前搬进易失的 JVM 内存。",[354,6343,6345,6346,2084,6349],{"id":6344},"使用高低水位控制-pause-和-resume","使用高、低水位控制 ",[119,6347,6348],{},"pause()",[119,6350,6351],{},"resume()",[18,6353,6354],{},"可以为队列设置容量和两个阈值。例如队列容量为 10000：",[204,6356,6359],{"className":6357,"code":6358,"language":209,"meta":210},[207],"高水位：8000\n队列达到高水位\n→ Poll 线程对相关 Partition 调用 pause()\n→ 后续 poll() 暂停返回这些 Partition 的新消息\n\n低水位：3000\n工作线程处理后，队列下降到低水位\n→ Poll 线程调用 resume()\n→ 后续 poll() 重新拉取这些 Partition\n",[119,6360,6358],{"__ignoreMap":210},[18,6362,6363],{},"使用两个不同阈值是为了避免队列在一个临界值附近反复暂停、恢复。阈值不能照搬固定数字，应根据单条消息大小、堆内存、工作线程数和峰值处理耗时进行容量评估。",[18,6365,6366,6368,6369,6371],{},[119,6367,6348],{}," 的含义是暂停指定 Partition 在后续 ",[119,6370,5981],{}," 中返回新记录，它不会：",[174,6373,6374,6377,6380,6383],{},[25,6375,6376],{},"取消当前 Partition 的分配。",[25,6378,6379],{},"自动提交 Offset。",[25,6381,6382],{},"主动触发 Consumer Group 重平衡。",[25,6384,6385],{},"清除已经拉取并放入本地队列的消息。",[18,6387,6388,6389,6391,6392,6394],{},"暂停期间仍然必须持续调用 ",[119,6390,5981],{},"，让 Consumer 处理心跳、协调和重平衡事件。不能因为所有分区都已暂停，就让 Poll 线程长时间睡眠或等待队列清空，否则仍可能超过 ",[119,6393,5994],{}," 并失去分区。",[354,6396,6398],{"id":6397},"只能由-consumer-所在线程控制拉取","只能由 Consumer 所在线程控制拉取",[18,6400,6401,6404,6405,892,6407,892,6409,6411],{},[119,6402,6403],{},"KafkaConsumer"," 不是线程安全的。通常由唯一的 Poll 线程持有并调用 ",[119,6406,5981],{},[119,6408,6348],{},[119,6410,6351],{}," 和提交 Offset；工作线程只处理业务，并把“完成结果”写回一个线程安全的完成队列。工作线程不能直接操作同一个 Consumer。",[18,6413,6414],{},"伪代码可以理解为：",[204,6416,6419],{"className":6417,"code":6418,"language":209,"meta":210},[207],"Poll 线程循环：\n    读取工作线程上报的完成结果\n    推进每个 Partition 的连续成功 Offset\n    根据队列水位执行 pause \u002F resume\n    poll() 拉取允许继续消费的 Partition\n    把记录投递到对应的有界工作队列\n\n工作线程：\n    处理业务\n    成功或失败结果写回完成队列\n    不直接调用 KafkaConsumer\n",[119,6420,6418],{"__ignoreMap":210},[354,6422,6424],{"id":6423},"为什么要按-partition-提交连续成功区间","为什么要按 Partition 提交“连续成功区间”",[18,6426,6427],{},"假设同一 Partition 拉取到：",[204,6429,6432],{"className":6430,"code":6431,"language":209,"meta":210},[207],"Offset 100、101、102\n",[119,6433,6431],{"__ignoreMap":210},[18,6435,6436],{},"并发处理结果是：",[204,6438,6441],{"className":6439,"code":6440,"language":209,"meta":210},[207],"100 成功\n101 仍在处理\n102 成功\n",[119,6442,6440],{"__ignoreMap":210},[18,6444,6445],{},"此时最多只能提交 101，也就是声明 100 已处理完成；不能提交 103。否则进程在 101 完成前崩溃，重启后会从 103 开始，101 就可能被永久跳过。",[18,6447,6448],{},"只有 101 也成功后，100～102 形成连续成功区间，才能提交 103。若业务要求同一 Partition 严格有序，最简单的方式是同一 Partition 串行处理，不要并发执行。",[354,6450,6451],{"id":6451},"重平衡时还要处理本地未完成任务",[18,6453,6454],{},"发生 Partition 撤销时，应停止向被撤销 Partition 的队列继续投递，并在回调允许的时间内提交已经连续处理成功的 Offset。尚未完成或来不及安全提交的任务，应允许新 Consumer 从旧 Offset 重新消费，因此业务处理必须具备幂等性。",[18,6456,6457],{},"需要共同遵守的约束包括：",[174,6459,6460,6465,6468,6475,6480,6483,6486],{},[25,6461,6462,6464],{},[119,6463,6403],{}," 本身不是线程安全的，不能让多个工作线程同时操作同一个 Consumer。",[25,6466,6467],{},"队列必须有界，不能把 Kafka 的积压无上限转移到 JVM 内存。",[25,6469,6470,5280,6472,6474],{},[119,6471,6348],{},[119,6473,6351],{}," 应由 Poll 线程执行，工作线程通过控制信号提出请求。",[25,6476,6477,6478,326],{},"即使 Partition 已暂停，Poll 线程也要持续执行 ",[119,6479,5981],{},[25,6481,6482],{},"Offset 必须在消息真正处理成功后提交。",[25,6484,6485],{},"同一 Partition 如果要求严格顺序，不能让后面的消息先于前面的消息产生业务效果。",[25,6487,6488],{},"并发完成后只能提交“连续成功区间”的下一个 Offset，不能跨过尚未完成或失败的消息。",[18,6490,6491],{},"异步化能提高吞吐，但会显著增加 Offset、顺序和失败处理的复杂度。",[14,6493,6495],{"id":6494},"五partition-不够时怎么办","五、Partition 不够时怎么办",[196,6497,6499],{"id":6498},"_1-增加-partition-主要提升未来并行度","1. 增加 Partition 主要提升未来并行度",[18,6501,6502],{},"如果 Consumer 数量已经等于 Partition 数量，且单分区处理能力也接近上限，可以评估增加 Partition。",[18,6504,6505],{},"但要明确：",[57,6507,6508],{},[18,6509,6510],{},[28,6511,6512],{},"给 Topic 新增 Partition，不会把旧 Partition 中已经积压的消息自动重新分布到新 Partition。",[18,6514,6515],{},"新 Partition 主要承接之后产生的新消息。旧积压仍然留在原来的 Partition 中，仍需由对应消费者追赶。",[196,6517,6519],{"id":6518},"_2-增加-partition-可能改变-key-路由","2. 增加 Partition 可能改变 Key 路由",[18,6521,6522],{},"默认的 Key 哈希结果通常与 Partition 数量有关。增加 Partition 后，同一个 Key 的新消息可能进入不同于历史消息的 Partition，从而影响跨扩容时点的顺序假设。",[18,6524,6525],{},"例如为了便于理解，假设某个分区规则可以简化为：",[204,6527,6530],{"className":6528,"code":6529,"language":209,"meta":210},[207],"目标分区 = hash(key) % Partition 数量\n",[119,6531,6529],{"__ignoreMap":210},[18,6533,6534],{},"同一个 Key 的哈希值为 13：",[204,6536,6539],{"className":6537,"code":6538,"language":209,"meta":210},[207],"原来 4 个 Partition：13 % 4 = 1，写入 P1\n扩容到 8 个 Partition：13 % 8 = 5，新消息写入 P5\n",[119,6540,6538],{"__ignoreMap":210},[18,6542,6543],{},"扩容前已经写入 P1 的旧消息不会被搬到 P5。P1 和 P5 又可能由不同 Consumer 并行处理，因此 P5 中的新消息可能先于 P1 中的旧消息完成。这个风险不只是“扩容瞬间”存在，而是会持续到旧分区中的相关历史消息全部处理完毕。",[18,6545,6546],{},"所谓“消费端能否接受跨分区处理”，实际是在问：",[57,6548,6549],{},[18,6550,6551],{},"同一个业务 Key 的旧消息和新消息同时位于不同 Partition，并可能并行、乱序完成时，业务是否仍然正确？",[18,6553,6554],{},"如果事件是独立日志或幂等的最终状态覆盖，业务可能可以接受；如果事件依赖严格顺序，例如“创建订单 → 支付成功 → 关闭订单”，就不能直接接受。",[18,6556,6557],{},"如果业务依赖同一 Key 的严格顺序，常见选择有：",[354,6559,6561],{"id":6560},"方案一排空旧消息后再扩分区","方案一：排空旧消息后再扩分区",[204,6563,6566],{"className":6564,"code":6565,"language":209,"meta":210},[207],"限制或暂停生产\n→ 等待旧 Topic 的相关消息处理完成\n→ 增加 Partition\n→ 恢复生产\n",[119,6567,6565],{"__ignoreMap":210},[18,6569,6570],{},"这种方式逻辑最清楚，但需要维护窗口，而且在高流量系统中可能很难完全停写。",[354,6572,6574],{"id":6573},"方案二新建-topic-并进行受控切换","方案二：新建 Topic 并进行受控切换",[18,6576,6577,6578,3213],{},"例如创建具有目标分区数和新路由规则的 ",[119,6579,6580],{},"order-events-v2",[204,6582,6585],{"className":6583,"code":6584,"language":209,"meta":210},[207],"1. 创建并验证新 Topic\n2. 定义明确的切换时间、版本号或业务水位\n3. 新消息从切换点开始写入 v2\n4. v1 消费者继续处理切换点以前的旧消息\n5. 确认某个 Key 或全部旧消息越过安全水位后，再允许 v2 对应消息生效\n6. 完成核对后停止 v1，并保留回滚方案\n",[119,6586,6584],{"__ignoreMap":210},[18,6588,6589],{},"新建 Topic 的价值是把新旧路由、配置和消费组隔离开，便于验证与回滚；代价是必须设计双 Topic 切换、重复消息、遗漏检查和新旧 Offset 衔接。它不是 Kafka 自动迁移，而是业务自己完成的版本化切换。",[18,6591,6592],{},"因此扩分区前应明确回答：",[174,6594,6595,6598,6601,6604],{},[25,6596,6597],{},"分区器如何计算目标 Partition。",[25,6599,6600],{},"旧消息是否已经处理完成。",[25,6602,6603],{},"同一 Key 跨新旧 Partition 并行处理是否会破坏业务顺序。",[25,6605,6606],{},"是选择维护窗口排空旧消息，还是新建 Topic 做受控切换。",[196,6608,6610],{"id":6609},"_3-极端情况下建立临时加速链路","3. 极端情况下建立临时加速链路",[18,6612,6613],{},"当原 Topic 分区数严重不足、积压量巨大时，可以设计经过评审的临时方案：",[204,6615,6618],{"className":6616,"code":6617,"language":209,"meta":210},[207],"原 Topic\n→ 临时搬运程序\n→ 更高分区数的中转 Topic\n→ 临时扩容后的消费组\n→ 业务处理\n",[119,6619,6617],{"__ignoreMap":210},[18,6621,6622],{},"这会引入顺序、重复、Offset 衔接和回切问题，只适合经过完整设计和演练的场景，不能在生产事故中临时拍脑袋执行。",[14,6624,6626],{"id":6625},"六热点-partition-如何处理","六、热点 Partition 如何处理",[18,6628,6629],{},"如果只有少数 Partition 积压，需要检查消息 Key 分布。",[18,6631,6632],{},"例如所有消息都使用同一个固定 Key：",[204,6634,6637],{"className":6635,"code":6636,"language":209,"meta":210},[207],"key = \"default\"\n→ 所有消息进入同一 Partition\n→ 其他 Partition 空闲\n→ 单分区成为瓶颈\n",[119,6638,6636],{"__ignoreMap":210},[18,6640,6641],{},"解决方向包括：",[174,6643,6644,6647,6650,6656,6659],{},[25,6645,6646],{},"选择分布更均匀的业务 Key。",[25,6648,6649],{},"对非顺序场景取消固定 Key。",[25,6651,6652,6653,326],{},"对热点业务 Key 做可控分片，例如 ",[119,6654,6655],{},"userId + shardNo",[25,6657,6658],{},"单独拆分热点租户或热点业务到独立 Topic。",[25,6660,6661],{},"优化该 Partition 对应消费者的单条处理耗时。",[18,6663,6664],{},"Key 拆分会改变顺序语义。原来同一 Key 的全局顺序，拆分后通常只能保证每个子 Key 内部有序。",[14,6666,6668],{"id":6667},"七毒消息和重试如何处理","七、毒消息和重试如何处理",[18,6670,6671],{},"不要让一条永远失败的消息无限阻塞 Partition。常见处理策略是：",[204,6673,6676],{"className":6674,"code":6675,"language":209,"meta":210},[207],"第一次失败\n→ 记录错误并有限重试\n→ 仍然失败\n→ 进入重试 Topic \u002F 死信 Topic\n→ 主消费链路继续\n→ 告警和人工补偿\n",[119,6677,6675],{"__ignoreMap":210},[18,6679,6680],{},"需要保留：",[174,6682,6683,6686,6689,6692],{},[25,6684,6685],{},"原始 Topic、Partition 和 Offset。",[25,6687,6688],{},"消息 Key、事件 ID 和原始内容。",[25,6690,6691],{},"异常原因和重试次数。",[25,6693,6694],{},"首次失败和最后失败时间。",[18,6696,6697],{},"如果业务要求同一 Key 严格顺序，就不能简单跳过失败消息继续处理后续消息。此时应暂停对应 Partition，优先修复或补偿该消息；这是吞吐与顺序之间的业务取舍。",[14,6699,6701],{"id":6700},"八如何估算多久能消化完积压","八、如何估算多久能消化完积压",[18,6703,6704],{},"设：",[204,6706,6709],{"className":6707,"code":6708,"language":209,"meta":210},[207],"B = 当前积压消息数\nP = 当前生产速度（条\u002F秒）\nC = 扩容后的稳定消费速度（条\u002F秒）\n",[119,6710,6708],{"__ignoreMap":210},[18,6712,6713],{},"只有：",[204,6715,6718],{"className":6716,"code":6717,"language":209,"meta":210},[207],"C > P\n",[119,6719,6717],{"__ignoreMap":210},[18,6721,6722],{},"积压才会下降。理论恢复时间约为：",[204,6724,6727],{"className":6725,"code":6726,"language":209,"meta":210},[207],"恢复时间 T = B \u002F (C - P)\n",[119,6728,6726],{"__ignoreMap":210},[18,6730,666],{},[204,6732,6735],{"className":6733,"code":6734,"language":209,"meta":210},[207],"当前积压 B = 360 万条\n生产速度 P = 2000 条\u002F秒\n消费速度 C = 5000 条\u002F秒\n\n净消化速度 = 5000 - 2000 = 3000 条\u002F秒\n理论恢复时间 = 3600000 \u002F 3000 = 1200 秒\n≈ 20 分钟\n",[119,6736,6734],{"__ignoreMap":210},[18,6738,6739],{},"实际还要为重平衡、失败重试、下游波动和流量峰值预留余量。",[18,6741,3234,6742,6745],{},[119,6743,6744],{},"C \u003C= P","，无论运行多久都追不上，必须降低生产速率或提高有效消费能力。",[14,6747,6749],{"id":6748},"九offset-跳到最新为什么不是常规方案","九、Offset 跳到最新为什么不是常规方案",[18,6751,6752],{},"把消费组 Offset 重置到最新位置，确实可以让 Lag 迅速变成 0，但其本质是：",[57,6754,6755],{},[18,6756,6757],{},[28,6758,6759],{},"主动放弃尚未消费的历史消息。",[18,6761,6762],{},"只有同时满足以下条件才可以考虑：",[174,6764,6765,6768,6771,6774,6777],{},[25,6766,6767],{},"业务明确确认旧消息可以丢弃。",[25,6769,6770],{},"已评估对账、状态和下游影响。",[25,6772,6773],{},"已保留必要的审计或补偿数据。",[25,6775,6776],{},"操作前进行了 Dry Run，并确认 Topic、Consumer Group 和目标 Offset。",[25,6778,6779],{},"操作期间停止相关消费者，避免 Offset 并发变化。",[18,6781,6782],{},"资金、订单、库存等关键链路不能把重置 Offset 当作普通清积压手段。",[196,6784,6786],{"id":6785},"什么是-dry-run","什么是 Dry Run",[18,6788,6789],{},"Dry Run 就是“只预演，不执行”。它先计算并展示每个 Partition 将从哪个 Offset 调整到哪个 Offset，供操作人员核对 Topic、消费组、分区范围和目标位置，但不真正修改已提交 Offset。",[18,6791,6792],{},"例如将消费组预演重置到最新位置：",[204,6794,6798],{"className":6795,"code":6796,"language":6797,"meta":210,"style":210},"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",[119,6799,6800,6810,6821,6831,6841,6848],{"__ignoreMap":210},[460,6801,6802,6806],{"class":462,"line":463},[460,6803,6805],{"class":6804},"sScJk","bin\u002Fkafka-consumer-groups.sh",[460,6807,6809],{"class":6808},"sj4cs"," \\\n",[460,6811,6812,6815,6819],{"class":462,"line":469},[460,6813,6814],{"class":6808},"  --bootstrap-server",[460,6816,6818],{"class":6817},"sZZnC"," kafka.example.com:9092",[460,6820,6809],{"class":6808},[460,6822,6823,6826,6829],{"class":462,"line":1158},[460,6824,6825],{"class":6808},"  --group",[460,6827,6828],{"class":6817}," order-consumer-group",[460,6830,6809],{"class":6808},[460,6832,6833,6836,6839],{"class":462,"line":2373},[460,6834,6835],{"class":6808},"  --topic",[460,6837,6838],{"class":6817}," order-events",[460,6840,6809],{"class":6808},[460,6842,6843,6846],{"class":462,"line":2379},[460,6844,6845],{"class":6808},"  --reset-offsets",[460,6847,6809],{"class":6808},[460,6849,6850],{"class":462,"line":2417},[460,6851,6852],{"class":6808},"  --to-latest\n",[18,6854,6855,6856,6859],{},"Kafka 4.1 的消费组工具默认只展示重置计划；确认结果无误后，显式增加 ",[119,6857,6858],{},"--execute"," 才会真正执行：",[204,6861,6863],{"className":6795,"code":6862,"language":6797,"meta":210,"style":210},"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",[119,6864,6865,6871,6879,6887,6895,6901,6908],{"__ignoreMap":210},[460,6866,6867,6869],{"class":462,"line":463},[460,6868,6805],{"class":6804},[460,6870,6809],{"class":6808},[460,6872,6873,6875,6877],{"class":462,"line":469},[460,6874,6814],{"class":6808},[460,6876,6818],{"class":6817},[460,6878,6809],{"class":6808},[460,6880,6881,6883,6885],{"class":462,"line":1158},[460,6882,6825],{"class":6808},[460,6884,6828],{"class":6817},[460,6886,6809],{"class":6808},[460,6888,6889,6891,6893],{"class":462,"line":2373},[460,6890,6835],{"class":6808},[460,6892,6838],{"class":6817},[460,6894,6809],{"class":6808},[460,6896,6897,6899],{"class":462,"line":2379},[460,6898,6845],{"class":6808},[460,6900,6809],{"class":6808},[460,6902,6903,6906],{"class":462,"line":2417},[460,6904,6905],{"class":6808},"  --to-latest",[460,6907,6809],{"class":6808},[460,6909,6910],{"class":462,"line":2423},[460,6911,6912],{"class":6808},"  --execute\n",[18,6914,6915],{},"不同 Kafka 版本的工具参数可能略有差异，生产操作前应以实际部署版本的命令帮助和官方文档为准。",[196,6917,6918],{"id":6918},"为什么操作期间要停止消费者",[18,6920,6921,6922,6924],{},"活动消费者会继续执行 ",[119,6923,5981],{},"、处理消息并提交 Offset。如果管理人员同时重置 Offset，就可能发生竞态：",[204,6926,6929],{"className":6927,"code":6928,"language":209,"meta":210},[207],"管理员把 Offset 从 1000 重置到 5000\n→ 仍在运行的消费者随后提交自己内存中的旧进度 1200\n→ 重置结果被旧提交覆盖\n",[119,6930,6928],{"__ignoreMap":210},[18,6932,6933],{},"反过来也可能是消费者刚处理到 1300，管理员基于稍早看到的状态执行重置，导致已处理消息重复消费或尚未处理消息被跳过。活动成员还可能触发重平衡，使操作时看到的分区归属和 Offset 继续变化。",[18,6935,6936],{},"因此安全流程应是：",[204,6938,6941],{"className":6939,"code":6940,"language":209,"meta":210},[207],"停止该消费组的所有消费者\n→ 确认消费组已无活动成员\n→ 记录操作前 Offset\n→ 执行 Dry Run 并双人核对\n→ 使用 --execute 修改 Offset\n→ 再次查询并确认结果\n→ 启动消费者并观察消费位置\n",[119,6942,6940],{"__ignoreMap":210},[18,6944,6945],{},"这不仅是避免并发覆盖。Kafka 官方的消费组 Offset 重置说明也明确要求先确保消费实例处于非活动状态。",[14,6947,6949],{"id":6948},"十恢复后要做什么","十、恢复后要做什么",[18,6951,6952],{},"积压恢复不代表问题结束，还应完成：",[22,6954,6955,6958,6961,6964,6967,6970,6973],{},[25,6956,6957],{},"复盘根因和时间线。",[25,6959,6960],{},"补充总 Lag、最大分区 Lag、时间 Lag 和增长率告警。",[25,6962,6963],{},"建立生产速度、消费速度和预计恢复时间看板。",[25,6965,6966],{},"对热点 Partition、毒消息和下游耗时建立专项监控。",[25,6968,6969],{},"按峰值流量进行容量压测，保留消费冗余。",[25,6971,6972],{},"演练消费者扩容、下游故障、重平衡和死信补偿。",[25,6974,6975],{},"核对积压期间是否发生消息过期、重复处理或业务遗漏。",[14,6977,6979],{"id":6978},"十一常见误区","十一、常见误区",[196,6981,6983],{"id":6982},"误区一积压就直接增加-consumer","误区一：积压就直接增加 Consumer",[18,6985,6986],{},"如果 Partition 数量不足、多数 Consumer 已空闲，或者瓶颈实际在数据库，增加 Consumer 不会解决问题，反而可能加剧下游压力。",[196,6988,6990,6991,6993],{"id":6989},"误区二把-maxpollrecords-调大就能提升吞吐","误区二：把 ",[119,6992,6296],{}," 调大就能提升吞吐",[18,6995,6996],{},"拉取更多消息不等于处理更快。批次过大还可能导致处理超时、重平衡和更高的失败重试成本。",[196,6998,7000],{"id":6999},"误区三增加-partition-会重新分配旧积压","误区三：增加 Partition 会重新分配旧积压",[18,7002,7003],{},"新增 Partition 不会迁移旧 Partition 中已有的消息，只能为后续消息提供更多并行空间。",[196,7005,7007],{"id":7006},"误区四lag-等于业务延迟","误区四：Lag 等于业务延迟",[18,7009,7010,7011,7014],{},"Lag 是 Offset 数量差，只表示还落后多少条记录，不等于时间延迟。相同 Lag 下，消费者处理速度越快，处理完现有积压所需的时间越短；如果生产仍在继续，则应使用 ",[119,7012,7013],{},"Lag ÷（消费速度－生产速度）"," 估算 Lag 归零时间。",[18,7016,7017],{},"当前业务已经延迟多久，应通过最老未处理消息的时间戳判断。消息大小影响积压字节量、网络与反序列化成本，单条处理耗时影响消费速度。因此排查时应把 Lag 条数、最老消息年龄、积压字节量和实际处理吞吐分开观察，不能混成一个指标。",[196,7019,7021],{"id":7020},"误区五把-offset-跳到最新就是清理积压","误区五：把 Offset 跳到最新就是清理积压",[18,7023,7024],{},"这不是“处理了积压”，而是“丢弃了积压”。必须经过明确的业务授权和风险评估。",[14,7026,1092],{"id":1092},[174,7028,7029,7034,7039,7046],{},[25,7030,7031],{},[70,7032,4680],{"href":4678,"rel":7033},[1101],[25,7035,7036],{},[70,7037,4694],{"href":4692,"rel":7038},[1101],[25,7040,7041],{},[70,7042,7045],{"href":7043,"rel":7044},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Foperations\u002Fconsumer-rebalance-protocol\u002F",[1101],"Apache Kafka Consumer Rebalance Protocol",[25,7047,7048],{},[70,7049,7052],{"href":7050,"rel":7051},"https:\u002F\u002Fkafka.apache.org\u002F41\u002Foperations\u002Fbasic-kafka-operations\u002F",[1101],"Apache Kafka Basic Operations",[1146,7054,7055],{},"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":210,"searchDepth":469,"depth":469,"links":7057},[7058,7059,7060,7061,7062,7069,7074,7080,7085,7086,7087,7088,7092,7093,7101],{"id":16,"depth":469,"text":16},{"id":66,"depth":469,"text":66},{"id":83,"depth":469,"text":83},{"id":5817,"depth":469,"text":5818},{"id":5944,"depth":469,"text":5945,"children":7063},[7064,7065,7066,7067,7068],{"id":5948,"depth":1158,"text":5949},{"id":5961,"depth":1158,"text":5962},{"id":5998,"depth":1158,"text":5999},{"id":6037,"depth":1158,"text":6038},{"id":6056,"depth":1158,"text":6057},{"id":6083,"depth":469,"text":6084,"children":7070},[7071,7072,7073],{"id":6087,"depth":1158,"text":6088},{"id":6103,"depth":1158,"text":6104},{"id":6139,"depth":1158,"text":6140},{"id":6152,"depth":469,"text":6153,"children":7075},[7076,7077,7078,7079],{"id":6156,"depth":1158,"text":6157},{"id":6175,"depth":1158,"text":6176},{"id":6253,"depth":1158,"text":6254},{"id":6319,"depth":1158,"text":6320},{"id":6494,"depth":469,"text":6495,"children":7081},[7082,7083,7084],{"id":6498,"depth":1158,"text":6499},{"id":6518,"depth":1158,"text":6519},{"id":6609,"depth":1158,"text":6610},{"id":6625,"depth":469,"text":6626},{"id":6667,"depth":469,"text":6668},{"id":6700,"depth":469,"text":6701},{"id":6748,"depth":469,"text":6749,"children":7089},[7090,7091],{"id":6785,"depth":1158,"text":6786},{"id":6918,"depth":1158,"text":6918},{"id":6948,"depth":469,"text":6949},{"id":6978,"depth":469,"text":6979,"children":7094},[7095,7096,7098,7099,7100],{"id":6982,"depth":1158,"text":6983},{"id":6989,"depth":1158,"text":7097},"误区二：把 max.poll.records 调大就能提升吞吐",{"id":6999,"depth":1158,"text":7000},{"id":7006,"depth":1158,"text":7007},{"id":7020,"depth":1158,"text":7021},{"id":1092,"depth":469,"text":1092},"wiki:java:kafka-message-backlog-handling","从 Consumer Lag 识别、瓶颈定位、限流止血、消费者扩容、批量处理、热点分区和毒消息治理等方面，系统讲解 Kafka 消息积压处理方案。",{},74,"\u002Fjava\u002Fkafka-message-backlog-handling",{"title":5768,"description":7103},"java\u002Fkafka-message-backlog-handling","JV6UjzzTuFU1AgBQpyXiVtkOWs_5BYokwcLCeslmZRA",{"id":7111,"title":7112,"body":7113,"commentId":8542,"description":8543,"difficulty":2636,"draft":1208,"extension":1209,"meta":8544,"navigation":1211,"order":8545,"path":8546,"section":1214,"seo":8547,"stem":8548,"updated":4749,"__hash__":8549},"java\u002Fjava\u002Fmysql-transaction-isolation-and-phantom-read.md","MySQL 的隔离级别都有哪些？如何避免幻读？",{"type":7,"value":7114,"toc":8493},[7115,7117,7120,7159,7162,7176,7179,7182,7187,7189,7198,7200,7204,7207,7212,7215,7218,7299,7301,7306,7310,7314,7317,7323,7329,7333,7336,7342,7345,7350,7354,7357,7363,7366,7369,7374,7377,7385,7389,7393,7396,7407,7410,7414,7417,7420,7426,7429,7435,7443,7447,7450,7456,7462,7465,7468,7472,7475,7481,7492,7495,7498,7502,7505,7509,7515,7524,7527,7538,7541,7545,7548,7572,7575,7578,7582,7585,7609,7616,7640,7643,7656,7659,7662,7667,7670,7674,7677,7704,7711,7720,7723,7726,7739,7746,7749,7754,7758,7762,7765,7767,7785,7791,7795,7798,7801,7807,7810,7816,7819,7823,7826,7832,7835,7841,7844,7850,7870,7873,7878,7881,7899,7912,7918,7921,7927,7930,7933,7957,7964,7967,7985,7989,7992,7995,8012,8015,8018,8021,8026,8028,8033,8036,8053,8057,8060,8080,8083,8086,8089,8103,8106,8121,8124,8128,8134,8137,8143,8146,8158,8161,8164,8168,8171,8179,8182,8190,8193,8207,8211,8215,8217,8231,8235,8237,8251,8255,8257,8271,8274,8294,8298,8301,8310,8313,8322,8325,8334,8337,8346,8349,8357,8360,8375,8386,8389,8397,8400,8402,8406,8409,8413,8416,8424,8428,8431,8435,8438,8442,8445,8449,8452,8454,8491],[14,7116,16],{"id":16},[18,7118,7119],{},"MySQL InnoDB 支持四种事务隔离级别：读未提交、读已提交、可重复读和串行化。",[174,7121,7122,7128,7134,7153],{},[25,7123,7124,7127],{},[28,7125,7126],{},"读未提交（READ UNCOMMITTED）","：可能发生脏读、不可重复读和幻读。",[25,7129,7130,7133],{},[28,7131,7132],{},"读已提交（READ COMMITTED）","：只能读取已经提交的数据，但每次普通查询都会创建新的 Read View，因此可能发生不可重复读和幻读。",[25,7135,7136,7139,7140,892,7143,892,7146,2084,7149,7152],{},[28,7137,7138],{},"可重复读（REPEATABLE READ）","：MySQL InnoDB 的默认隔离级别。同一事务中的普通快照读复用第一次查询建立的 Read View，避免不可重复读，并通过一致性快照避免看到后来插入的幻影行；对于 ",[119,7141,7142],{},"SELECT ... FOR UPDATE",[119,7144,7145],{},"SELECT ... FOR SHARE",[119,7147,7148],{},"UPDATE",[119,7150,7151],{},"DELETE"," 等当前读，则通过 Next-Key Lock 锁住索引记录及其前面的间隙，阻止其他事务在查询范围内插入新记录。",[25,7154,7155,7158],{},[28,7156,7157],{},"串行化（SERIALIZABLE）","：隔离性最强，事务近似串行执行，可以避免脏读、不可重复读和幻读，但并发能力最低。",[18,7160,7161],{},"因此，回答“InnoDB 如何避免幻读”时，需要区分两类读取：",[22,7163,7164,7171],{},[25,7165,7166,7167,7170],{},"普通 ",[119,7168,7169],{},"SELECT"," 属于快照读，REPEATABLE READ 通过 MVCC 和同一个 Read View，让事务始终读取同一份一致性快照。",[25,7172,7173,7175],{},[119,7174,7142],{}," 等属于当前读，REPEATABLE READ 通过 Gap Lock 和 Next-Key Lock 阻止其他事务向锁定范围插入新记录。",[18,7177,7178],{},"如果业务逻辑是“先查询是否存在，再决定是否插入或更新”，仅依赖快照读通常不够，应在 REPEATABLE READ 下使用合适索引和锁定读，并通过唯一索引等数据库约束作为最终兜底。",[196,7180,7181],{"id":7181},"一句话总结",[57,7183,7184],{},[18,7185,7186],{},"InnoDB 默认使用 REPEATABLE READ：普通查询靠 MVCC 的一致性快照避免看见幻影行，当前读靠 Next-Key Lock 锁住记录和间隙，阻止幻影行被插入。",[14,7188,66],{"id":66},[18,7190,7191],{},[70,7192,7195],{"href":7193,"target":73,"rel":7194},"\u002Fimages\u002Fwiki\u002Fjava\u002Fmysql-transaction-isolation-and-phantom-read.svg",[75,76],[78,7196],{"src":7193,"alt":7197},"MySQL 四种事务隔离级别与 InnoDB 避免幻读的机制",[14,7199,83],{"id":83},[14,7201,7203],{"id":7202},"一事务隔离级别解决什么问题","一、事务隔离级别解决什么问题",[18,7205,7206],{},"多个事务并发执行时，一个事务的写入可能影响另一个事务的读取。隔离级别用于规定：",[57,7208,7209],{},[18,7210,7211],{},"一个事务能够看到其他事务的哪些修改，以及什么时候能够看到这些修改。",[18,7213,7214],{},"隔离级别越高，事务之间的影响越小，但加锁和等待通常越多，并发能力也可能越低。",[18,7216,7217],{},"MySQL InnoDB 支持 SQL 标准定义的四种隔离级别：",[92,7219,7220,7240],{},[95,7221,7222],{},[98,7223,7224,7227,7231,7234,7237],{},[101,7225,7226],{},"隔离级别",[101,7228,7230],{"align":7229},"right","脏读",[101,7232,7233],{"align":7229},"不可重复读",[101,7235,7236],{"align":7229},"幻读",[101,7238,7239],{"align":7229},"并发能力",[111,7241,7242,7257,7271,7285],{},[98,7243,7244,7247,7250,7252,7254],{},[116,7245,7246],{},"READ UNCOMMITTED",[116,7248,7249],{"align":7229},"可能",[116,7251,7249],{"align":7229},[116,7253,7249],{"align":7229},[116,7255,7256],{"align":7229},"最高",[98,7258,7259,7262,7265,7267,7269],{},[116,7260,7261],{},"READ COMMITTED",[116,7263,7264],{"align":7229},"避免",[116,7266,7249],{"align":7229},[116,7268,7249],{"align":7229},[116,7270,2889],{"align":7229},[98,7272,7273,7276,7278,7280,7283],{},[116,7274,7275],{},"REPEATABLE READ",[116,7277,7264],{"align":7229},[116,7279,7264],{"align":7229},[116,7281,7282],{"align":7229},"InnoDB 通过 MVCC 和 Next-Key Lock 处理",[116,7284,2889],{"align":7229},[98,7286,7287,7290,7292,7294,7296],{},[116,7288,7289],{},"SERIALIZABLE",[116,7291,7264],{"align":7229},[116,7293,7264],{"align":7229},[116,7295,7264],{"align":7229},[116,7297,7298],{"align":7229},"最低",[18,7300,3785],{},[57,7302,7303],{},[18,7304,7305],{},"“REPEATABLE READ 可能发生幻读”是 SQL 标准层面的常见结论；MySQL InnoDB 在该级别上又实现了 MVCC 和 Next-Key Lock，对快照读和当前读分别处理幻读问题。",[14,7307,7309],{"id":7308},"二三种并发读取问题","二、三种并发读取问题",[196,7311,7313],{"id":7312},"_1-脏读","1. 脏读",[18,7315,7316],{},"事务 A 读取了事务 B 尚未提交的数据。如果事务 B 最后回滚，事务 A 读到的就是从未真正生效的数据。",[204,7318,7321],{"className":7319,"code":7320,"language":209,"meta":210},[207],"事务 A                         事务 B\n                              修改余额 100 → 0\n读取余额，得到 0\n                              ROLLBACK\n",[119,7322,7320],{"__ignoreMap":210},[18,7324,7325,7326,7328],{},"这里的 ",[119,7327,1473],{}," 就是脏数据。",[196,7330,7332],{"id":7331},"_2-不可重复读","2. 不可重复读",[18,7334,7335],{},"同一事务两次读取同一行，得到的字段值不同。",[204,7337,7340],{"className":7338,"code":7339,"language":209,"meta":210},[207],"事务 A                         事务 B\n读取 id=1，余额为 100\n                              修改余额为 80\n                              COMMIT\n再次读取 id=1，余额为 80\n",[119,7341,7339],{"__ignoreMap":210},[18,7343,7344],{},"不可重复读关注的是：",[57,7346,7347],{},[18,7348,7349],{},"同一行记录的内容发生了变化。",[196,7351,7353],{"id":7352},"_3-幻读","3. 幻读",[18,7355,7356],{},"同一事务使用相同查询条件执行两次查询，第二次返回的结果集合中出现了第一次不存在的记录，或者原有记录消失。",[204,7358,7361],{"className":7359,"code":7360,"language":209,"meta":210},[207],"事务 A                         事务 B\n查询 amount > 100，返回 3 行\n                              插入一条 amount=150 的记录\n                              COMMIT\n再次查询 amount > 100，返回 4 行\n",[119,7362,7360],{"__ignoreMap":210},[18,7364,7365],{},"新增的第 4 行就是幻影行。",[18,7367,7368],{},"幻读关注的是：",[57,7370,7371],{},[18,7372,7373],{},"满足查询条件的记录集合发生了变化。",[18,7375,7376],{},"所以，不可重复读和幻读的区别可以记为：",[174,7378,7379,7382],{},[25,7380,7381],{},"不可重复读：同一行的值变了。",[25,7383,7384],{},"幻读：符合条件的行数或记录集合变了。",[14,7386,7388],{"id":7387},"三四种隔离级别","三、四种隔离级别",[196,7390,7392],{"id":7391},"_1-read-uncommitted读未提交","1. READ UNCOMMITTED：读未提交",[18,7394,7395],{},"事务可以读取其他事务尚未提交的修改，因此可能发生：",[174,7397,7398,7401,7404],{},[25,7399,7400],{},"脏读；",[25,7402,7403],{},"不可重复读；",[25,7405,7406],{},"幻读。",[18,7408,7409],{},"它的隔离性最低，实际业务系统很少使用。",[196,7411,7413],{"id":7412},"_2-read-committed读已提交","2. READ COMMITTED：读已提交",[18,7415,7416],{},"事务只能读取已经提交的数据，因此不会发生脏读。",[18,7418,7419],{},"但在 InnoDB 中，每一次普通一致性读都会创建一个新的 Read View：",[204,7421,7424],{"className":7422,"code":7423,"language":209,"meta":210},[207],"第一次 SELECT → Read View 1\n第二次 SELECT → Read View 2\n第三次 SELECT → Read View 3\n",[119,7425,7423],{"__ignoreMap":210},[18,7427,7428],{},"如果其他事务在两次查询之间提交了修改，后一次查询就可能看到新数据，因此可能发生：",[174,7430,7431,7433],{},[25,7432,7403],{},[25,7434,7406],{},[18,7436,7437,7438,2084,7440,7442],{},"另外，在 READ COMMITTED 下，锁定读、",[119,7439,7148],{},[119,7441,7151],{}," 通常只锁索引记录，不锁记录前面的间隙。除了外键约束检查和重复键检查等情况外，Gap Lock 被禁用，因此其他事务仍可能向间隙插入新记录。",[196,7444,7446],{"id":7445},"_3-repeatable-read可重复读","3. REPEATABLE READ：可重复读",[18,7448,7449],{},"这是 InnoDB 的默认隔离级别。",[18,7451,7452,7453,7455],{},"对于普通 ",[119,7454,7169],{},"，事务中的第一次一致性读会建立 Read View，之后的普通一致性读继续使用该快照：",[204,7457,7460],{"className":7458,"code":7459,"language":209,"meta":210},[207],"第一次普通 SELECT → 创建 Read View\n第二次普通 SELECT → 复用同一个 Read View\n第三次普通 SELECT → 复用同一个 Read View\n",[119,7461,7459],{"__ignoreMap":210},[18,7463,7464],{},"因此，其他事务之后提交的更新、删除或插入不会出现在当前事务的旧快照中。",[18,7466,7467],{},"对于锁定读和数据修改语句，InnoDB 会读取最新可用数据，并根据查询使用的索引范围加锁。范围条件通常会使用 Gap Lock 或 Next-Key Lock，阻止其他事务向被锁定的范围插入新记录。",[196,7469,7471],{"id":7470},"_4-serializable串行化","4. SERIALIZABLE：串行化",[18,7473,7474],{},"SERIALIZABLE 是隔离性最强的级别。",[18,7476,7477,7478,7480],{},"在关闭自动提交的情况下，InnoDB 会把普通 ",[119,7479,7169],{}," 隐式转换为共享锁定读，效果类似：",[204,7482,7486],{"className":7483,"code":7484,"language":7485,"meta":210,"style":210},"language-sql shiki shiki-themes github-light github-dark","SELECT ... FOR SHARE;\n","sql",[119,7487,7488],{"__ignoreMap":210},[460,7489,7490],{"class":462,"line":463},[460,7491,7484],{},[18,7493,7494],{},"这样事务之间会产生更多锁等待，能够避免脏读、不可重复读和幻读，但吞吐量通常明显下降。",[18,7496,7497],{},"它适合一致性要求极高、并发量较低，或者确实需要事务串行执行的场景。",[14,7499,7501],{"id":7500},"四快照读与当前读","四、快照读与当前读",[18,7503,7504],{},"理解 InnoDB 如何处理幻读，必须先区分快照读和当前读。",[196,7506,7508],{"id":7507},"_1-快照读","1. 快照读",[18,7510,7511,7512,7514],{},"在 READ COMMITTED 和 REPEATABLE READ 下，普通 ",[119,7513,7169],{}," 通常属于快照读：",[204,7516,7518],{"className":7483,"code":7517,"language":7485,"meta":210,"style":210},"SELECT * FROM orders WHERE amount BETWEEN 100 AND 200;\n",[119,7519,7520],{"__ignoreMap":210},[460,7521,7522],{"class":462,"line":463},[460,7523,7517],{},[18,7525,7526],{},"快照读：",[174,7528,7529,7532,7535],{},[25,7530,7531],{},"不对查询记录加行锁；",[25,7533,7534],{},"通过 MVCC 读取某个时间点的一致性数据；",[25,7536,7537],{},"其他事务可以继续修改、删除或插入数据。",[18,7539,7540],{},"REPEATABLE READ 下，同一事务中的普通快照读复用同一个 Read View，所以后来提交的记录对当前快照不可见。",[196,7542,7544],{"id":7543},"_2-当前读","2. 当前读",[18,7546,7547],{},"下面这些操作需要读取最新可用的数据，并对数据加锁：",[204,7549,7551],{"className":7483,"code":7550,"language":7485,"meta":210,"style":210},"SELECT ... FOR UPDATE;\nSELECT ... FOR SHARE;\nUPDATE ...;\nDELETE ...;\n",[119,7552,7553,7558,7562,7567],{"__ignoreMap":210},[460,7554,7555],{"class":462,"line":463},[460,7556,7557],{},"SELECT ... FOR UPDATE;\n",[460,7559,7560],{"class":462,"line":469},[460,7561,7484],{},[460,7563,7564],{"class":462,"line":1158},[460,7565,7566],{},"UPDATE ...;\n",[460,7568,7569],{"class":462,"line":2373},[460,7570,7571],{},"DELETE ...;\n",[18,7573,7574],{},"它们通常被称为当前读或锁定读。",[18,7576,7577],{},"当前读不能只依靠旧快照，因为业务接下来可能要修改数据。它必须在最新数据状态上进行判断，并锁住相关记录或范围，避免判断完成后数据立即被并发事务改变。",[14,7579,7581],{"id":7580},"五快照读如何避免幻读","五、快照读如何避免幻读",[18,7583,7584],{},"假设当前使用 REPEATABLE READ：",[204,7586,7588],{"className":7483,"code":7587,"language":7485,"meta":210,"style":210},"START TRANSACTION;\n\nSELECT * FROM orders\nWHERE amount BETWEEN 100 AND 200;\n",[119,7589,7590,7595,7599,7604],{"__ignoreMap":210},[460,7591,7592],{"class":462,"line":463},[460,7593,7594],{},"START TRANSACTION;\n",[460,7596,7597],{"class":462,"line":469},[460,7598,2400],{"emptyLinePlaceholder":1211},[460,7600,7601],{"class":462,"line":1158},[460,7602,7603],{},"SELECT * FROM orders\n",[460,7605,7606],{"class":462,"line":2373},[460,7607,7608],{},"WHERE amount BETWEEN 100 AND 200;\n",[18,7610,7611,7612,7615],{},"第一次普通查询建立 Read View。此后事务 B 插入一条 ",[119,7613,7614],{},"amount=150"," 的记录并提交：",[204,7617,7619],{"className":7483,"code":7618,"language":7485,"meta":210,"style":210},"INSERT INTO orders(user_id, amount)\nVALUES (10, 150);\n\nCOMMIT;\n",[119,7620,7621,7626,7631,7635],{"__ignoreMap":210},[460,7622,7623],{"class":462,"line":463},[460,7624,7625],{},"INSERT INTO orders(user_id, amount)\n",[460,7627,7628],{"class":462,"line":469},[460,7629,7630],{},"VALUES (10, 150);\n",[460,7632,7633],{"class":462,"line":1158},[460,7634,2400],{"emptyLinePlaceholder":1211},[460,7636,7637],{"class":462,"line":2373},[460,7638,7639],{},"COMMIT;\n",[18,7641,7642],{},"事务 A 再次执行相同的普通查询：",[204,7644,7646],{"className":7483,"code":7645,"language":7485,"meta":210,"style":210},"SELECT * FROM orders\nWHERE amount BETWEEN 100 AND 200;\n",[119,7647,7648,7652],{"__ignoreMap":210},[460,7649,7650],{"class":462,"line":463},[460,7651,7603],{},[460,7653,7654],{"class":462,"line":469},[460,7655,7608],{},[18,7657,7658],{},"由于事务 A 仍然使用原来的 Read View，新插入的记录不在该快照的可见范围内，因此第二次查询不会看到它。",[18,7660,7661],{},"这里需要准确理解：",[57,7663,7664],{},[18,7665,7666],{},"MVCC 没有阻止事务 B 插入记录，只是让这条后来提交的记录对事务 A 的旧快照不可见。",[18,7668,7669],{},"因此，快照读解决的是“当前事务看到什么”，而不是“其他事务能不能写入”。",[14,7671,7673],{"id":7672},"六当前读如何避免幻读","六、当前读如何避免幻读",[18,7675,7676],{},"如果业务需要先锁定一个范围，再根据查询结果修改数据，可以使用：",[204,7678,7680],{"className":7483,"code":7679,"language":7485,"meta":210,"style":210},"START TRANSACTION;\n\nSELECT * FROM orders\nWHERE amount BETWEEN 100 AND 200\nFOR UPDATE;\n",[119,7681,7682,7686,7690,7694,7699],{"__ignoreMap":210},[460,7683,7684],{"class":462,"line":463},[460,7685,7594],{},[460,7687,7688],{"class":462,"line":469},[460,7689,2400],{"emptyLinePlaceholder":1211},[460,7691,7692],{"class":462,"line":1158},[460,7693,7603],{},[460,7695,7696],{"class":462,"line":2373},[460,7697,7698],{},"WHERE amount BETWEEN 100 AND 200\n",[460,7700,7701],{"class":462,"line":2379},[460,7702,7703],{},"FOR UPDATE;\n",[18,7705,7706,7707,7710],{},"假设 ",[119,7708,7709],{},"amount"," 上存在普通索引：",[204,7712,7714],{"className":7483,"code":7713,"language":7485,"meta":210,"style":210},"CREATE INDEX idx_orders_amount ON orders(amount);\n",[119,7715,7716],{"__ignoreMap":210},[460,7717,7718],{"class":462,"line":463},[460,7719,7713],{},[18,7721,7722],{},"在 REPEATABLE READ 下，InnoDB 扫描这个索引范围时，通常会对索引记录及其间隙加 Next-Key Lock。",[18,7724,7725],{},"此时另一个事务尝试插入：",[204,7727,7729],{"className":7483,"code":7728,"language":7485,"meta":210,"style":210},"INSERT INTO orders(user_id, amount)\nVALUES (10, 150);\n",[119,7730,7731,7735],{"__ignoreMap":210},[460,7732,7733],{"class":462,"line":463},[460,7734,7625],{},[460,7736,7737],{"class":462,"line":469},[460,7738,7630],{},[18,7740,7741,7742,7745],{},"由于 ",[119,7743,7744],{},"150"," 落在已经锁定的索引范围内，该插入通常会被阻塞，直到前一个事务提交或回滚。",[18,7747,7748],{},"这次不是“让新记录不可见”，而是：",[57,7750,7751],{},[18,7752,7753],{},"阻止新记录进入已经锁定的查询范围。",[14,7755,7757],{"id":7756},"七record-lockgap-lock-与-next-key-lock","七、Record Lock、Gap Lock 与 Next-Key Lock",[196,7759,7761],{"id":7760},"_1-record-lock","1. Record Lock",[18,7763,7764],{},"Record Lock 锁住的是索引记录。",[18,7766,666],{},[204,7768,7770],{"className":7483,"code":7769,"language":7485,"meta":210,"style":210},"SELECT * FROM orders\nWHERE id = 100\nFOR UPDATE;\n",[119,7771,7772,7776,7781],{"__ignoreMap":210},[460,7773,7774],{"class":462,"line":463},[460,7775,7603],{},[460,7777,7778],{"class":462,"line":469},[460,7779,7780],{},"WHERE id = 100\n",[460,7782,7783],{"class":462,"line":1158},[460,7784,7703],{},[18,7786,3234,7787,7790],{},[119,7788,7789],{},"id"," 是唯一索引，并且使用唯一等值条件准确定位一条记录，InnoDB 通常只锁找到的索引记录，不需要锁前面的间隙。",[196,7792,7794],{"id":7793},"_2-gap-lock","2. Gap Lock",[18,7796,7797],{},"Gap Lock 锁住的是两个索引记录之间的间隙，而不是某一条已经存在的记录。",[18,7799,7800],{},"假设索引中已有：",[204,7802,7805],{"className":7803,"code":7804,"language":209,"meta":210},[207],"100\n200\n300\n",[119,7806,7804],{"__ignoreMap":210},[18,7808,7809],{},"那么其中存在：",[204,7811,7814],{"className":7812,"code":7813,"language":209,"meta":210},[207],"(-∞, 100)\n(100, 200)\n(200, 300)\n(300, +∞)\n",[119,7815,7813],{"__ignoreMap":210},[18,7817,7818],{},"Gap Lock 的主要作用是限制其他事务在对应间隙插入新索引记录。",[196,7820,7822],{"id":7821},"_3-next-key-lock","3. Next-Key Lock",[18,7824,7825],{},"Next-Key Lock 的准确定义是：",[204,7827,7830],{"className":7828,"code":7829,"language":209,"meta":210},[207],"Next-Key Lock = Record Lock + 该记录前面的 Gap Lock\n",[119,7831,7829],{"__ignoreMap":210},[18,7833,7834],{},"这里的“前面”是指索引排序中的前一个间隙。假设索引值为：",[204,7836,7839],{"className":7837,"code":7838,"language":209,"meta":210},[207],"3\n5\n8\n",[119,7840,7838],{"__ignoreMap":210},[18,7842,7843],{},"可能形成的 Next-Key Lock 区间是：",[204,7845,7848],{"className":7846,"code":7847,"language":209,"meta":210},[207],"(-∞, 3]\n(3, 5]\n(5, 8]\n(8, +∞)\n",[119,7849,7847],{"__ignoreMap":210},[18,7851,7852,7853,7856,7857,7860,7861,7863,7864,5280,7867,7869],{},"例如，对索引记录 ",[119,7854,7855],{},"5"," 加 Next-Key Lock，可以概念性地理解为锁住 ",[119,7858,7859],{},"(3, 5]","：既保护记录 ",[119,7862,7855],{},"，又阻止其他事务向 ",[119,7865,7866],{},"3",[119,7868,7855],{}," 之间插入新索引记录。",[18,7871,7872],{},"这里需要区分单个 Next-Key Lock 和一条 SQL 最终获得的整组锁：",[57,7874,7875],{},[18,7876,7877],{},"一个 Next-Key Lock 只包含当前记录和它前面的间隙，但一条 SQL 可能同时获得多个 Record Lock、Gap Lock 和 Next-Key Lock，最终覆盖查询记录前后的整个范围。",[18,7879,7880],{},"例如，在 REPEATABLE READ 下执行：",[204,7882,7884],{"className":7483,"code":7883,"language":7485,"meta":210,"style":210},"SELECT * FROM orders\nWHERE amount = 5\nFOR UPDATE;\n",[119,7885,7886,7890,7895],{"__ignoreMap":210},[460,7887,7888],{"class":462,"line":463},[460,7889,7603],{},[460,7891,7892],{"class":462,"line":469},[460,7893,7894],{},"WHERE amount = 5\n",[460,7896,7897],{"class":462,"line":1158},[460,7898,7703],{},[18,7900,3234,7901,7903,7904,7907,7908,7911],{},[119,7902,7709],{}," 是普通非唯一索引，并且相邻索引值为 ",[119,7905,7906],{},"3、5、8","，为了防止其他事务再次插入 ",[119,7909,7910],{},"amount=5"," 的记录，最终需要保护：",[204,7913,7916],{"className":7914,"code":7915,"language":209,"meta":210},[207],"记录 5\n记录 5 前面的间隙：(3, 5)\n记录 5 后面的间隙：(5, 8)\n",[119,7917,7915],{"__ignoreMap":210},[18,7919,7920],{},"可以概念性地理解为：",[204,7922,7925],{"className":7923,"code":7924,"language":209,"meta":210},[207],"(3, 5]  → 记录 5 的 Next-Key Lock\n(5, 8)  → 查询范围结束位置的 Gap Lock\n",[119,7926,7924],{"__ignoreMap":210},[18,7928,7929],{},"所以，“等值查询可能锁住匹配记录及其前后间隙”是对一条 SQL 最终锁定范围的描述；它不代表单个 Next-Key Lock 同时包含记录前后两个间隙。",[18,7931,7932],{},"还要区分以下情况：",[174,7934,7935,7940,7951,7954],{},[25,7936,7166,7937,7939],{},[119,7938,7169],{}," 是快照读，不会因为这个查询条件加 Next-Key Lock。",[25,7941,7942,892,7944,892,7946,2084,7948,7950],{},[119,7943,7142],{},[119,7945,7145],{},[119,7947,7148],{},[119,7949,7151],{}," 才属于锁定读或当前读。",[25,7952,7953],{},"如果使用唯一索引和唯一等值条件，并且准确找到已有记录，InnoDB 通常只加 Record Lock，不需要锁前后间隙。",[25,7955,7956],{},"如果查询条件没有命中唯一记录，或者使用普通非唯一索引，InnoDB 会根据实际扫描范围使用 Gap Lock 或 Next-Key Lock。",[18,7958,7959,7960,7963],{},"具体获得哪些锁，最终还取决于隔离级别、索引类型、查询条件和实际执行计划，不能只根据 ",[119,7961,7962],{},"WHERE"," 条件判断。",[18,7965,7966],{},"InnoDB 还可以锁住最后一条记录之后的间隙，从而保护开放区间查询：",[204,7968,7970],{"className":7483,"code":7969,"language":7485,"meta":210,"style":210},"SELECT * FROM orders\nWHERE amount > 200\nFOR UPDATE;\n",[119,7971,7972,7976,7981],{"__ignoreMap":210},[460,7973,7974],{"class":462,"line":463},[460,7975,7603],{},[460,7977,7978],{"class":462,"line":469},[460,7979,7980],{},"WHERE amount > 200\n",[460,7982,7983],{"class":462,"line":1158},[460,7984,7703],{},[14,7986,7988],{"id":7987},"八索引为什么很重要","八、索引为什么很重要",[18,7990,7991],{},"InnoDB 的行锁本质上是索引记录锁，Next-Key Lock 也是围绕索引扫描范围建立的。",[18,7993,7994],{},"如果查询能够使用合适索引：",[204,7996,7998],{"className":7483,"code":7997,"language":7485,"meta":210,"style":210},"SELECT * FROM orders\nWHERE amount BETWEEN 100 AND 200\nFOR UPDATE;\n",[119,7999,8000,8004,8008],{"__ignoreMap":210},[460,8001,8002],{"class":462,"line":463},[460,8003,7603],{},[460,8005,8006],{"class":462,"line":469},[460,8007,7698],{},[460,8009,8010],{"class":462,"line":1158},[460,8011,7703],{},[18,8013,8014],{},"InnoDB 可以更准确地锁定相关索引范围。",[18,8016,8017],{},"如果缺少合适索引，InnoDB 可能需要扫描并锁定大量索引记录，锁的范围会变大，并发能力明显下降。",[18,8019,8020],{},"这里不要简单表述为：",[57,8022,8023],{},[18,8024,8025],{},"没有索引就一定加表锁。",[18,8027,1728],{},[57,8029,8030],{},[18,8031,8032],{},"InnoDB 仍然基于索引记录加锁；缺少可用业务索引时，可能扫描并锁住大量记录，使效果接近大范围锁定。",[18,8034,8035],{},"因此，使用锁定读避免幻读时，需要同时关注：",[174,8037,8038,8041,8044,8047,8050],{},[25,8039,8040],{},"查询条件是否命中索引；",[25,8042,8043],{},"实际执行计划选择了哪个索引；",[25,8045,8046],{},"锁定范围是否超出预期；",[25,8048,8049],{},"事务是否足够短；",[25,8051,8052],{},"是否可能形成死锁。",[14,8054,8056],{"id":8055},"九为什么不能只依赖快照读完成先查后写","九、为什么不能只依赖快照读完成“先查后写”",[18,8058,8059],{},"假设业务要求同一用户只能存在一条有效申请：",[204,8061,8063],{"className":7483,"code":8062,"language":7485,"meta":210,"style":210},"SELECT * FROM applications\nWHERE user_id = 100\n  AND status = 'ACTIVE';\n",[119,8064,8065,8070,8075],{"__ignoreMap":210},[460,8066,8067],{"class":462,"line":463},[460,8068,8069],{},"SELECT * FROM applications\n",[460,8071,8072],{"class":462,"line":469},[460,8073,8074],{},"WHERE user_id = 100\n",[460,8076,8077],{"class":462,"line":1158},[460,8078,8079],{},"  AND status = 'ACTIVE';\n",[18,8081,8082],{},"两个事务可能同时通过快照读发现“记录不存在”，然后各自插入一条有效申请。",[18,8084,8085],{},"REPEATABLE READ 虽然让两个事务各自的查询结果保持一致，但没有阻止另一个事务执行插入。",[18,8087,8088],{},"因此，业务不变量不能只依赖普通快照读。常见方案是：",[22,8090,8091,8094,8097,8100],{},[25,8092,8093],{},"建立能够表达业务规则的唯一索引；",[25,8095,8096],{},"在 REPEATABLE READ 下使用合适的锁定读；",[25,8098,8099],{},"捕获重复键或死锁异常，并进行有限重试；",[25,8101,8102],{},"缩短事务时间，避免锁范围长期占用。",[18,8104,8105],{},"例如业务规则能够转换为唯一键时，优先让数据库约束兜底：",[204,8107,8109],{"className":7483,"code":8108,"language":7485,"meta":210,"style":210},"CREATE UNIQUE INDEX uk_user_active\nON applications(user_id, active_flag);\n",[119,8110,8111,8116],{"__ignoreMap":210},[460,8112,8113],{"class":462,"line":463},[460,8114,8115],{},"CREATE UNIQUE INDEX uk_user_active\n",[460,8117,8118],{"class":462,"line":469},[460,8119,8120],{},"ON applications(user_id, active_flag);\n",[18,8122,8123],{},"具体索引设计仍要根据数据模型确定，不能为了唯一约束直接照搬示例。",[14,8125,8127],{"id":8126},"十混用快照读和当前读为什么容易困惑","十、混用快照读和当前读为什么容易困惑",[18,8129,8130,8131,8133],{},"在 REPEATABLE READ 事务中，普通 ",[119,8132,7169],{}," 使用旧快照，而锁定读读取最新可用的数据。",[18,8135,8136],{},"可能出现：",[204,8138,8141],{"className":8139,"code":8140,"language":209,"meta":210},[207],"1. 普通 SELECT 没看到某条新记录\n2. 另一个事务插入该记录并提交\n3. SELECT ... FOR UPDATE 却看到了这条记录\n",[119,8142,8140],{"__ignoreMap":210},[18,8144,8145],{},"原因不是隔离级别突然失效，而是两条语句使用了不同的读取机制：",[174,8147,8148,8153],{},[25,8149,7166,8150,8152],{},[119,8151,7169],{},"：读取 Read View 中的一致性快照；",[25,8154,8155,8157],{},[119,8156,7142],{},"：读取最新可用数据并加锁。",[18,8159,8160],{},"MySQL 官方文档也不建议在同一个 REPEATABLE READ 事务中随意混用锁定语句和非锁定查询，因为它们可能呈现两个不同时间状态的数据。",[18,8162,8163],{},"如果业务必须基于最新结果做写入判断，应统一使用锁定读；如果必须获得更严格、容易理解的串行语义，可以评估 SERIALIZABLE，但要接受更高的锁竞争。",[14,8165,8167],{"id":8166},"十一read-committed-下加-for-update-能否避免幻读","十一、READ COMMITTED 下加 FOR UPDATE 能否避免幻读",[18,8169,8170],{},"不能笼统地说可以。",[18,8172,8173,8174,2084,8176,8178],{},"在 READ COMMITTED 下，InnoDB 对锁定读、",[119,8175,7148],{},[119,8177,7151],{}," 通常只锁索引记录，不锁前面的间隙。其他事务仍可以向记录间隙插入新行，因此范围查询仍可能出现幻读。",[18,8180,8181],{},"Gap Lock 在该级别主要保留给：",[174,8183,8184,8187],{},[25,8185,8186],{},"外键约束检查；",[25,8188,8189],{},"重复键检查。",[18,8191,8192],{},"如果业务需要锁住一个尚不存在的范围，应评估：",[174,8194,8195,8198,8201,8204],{},[25,8196,8197],{},"使用 REPEATABLE READ，并通过合适索引和 Next-Key Lock 锁定范围；",[25,8199,8200],{},"使用 SERIALIZABLE；",[25,8202,8203],{},"把业务不变量转换为唯一约束；",[25,8205,8206],{},"调整数据模型，避免依赖无法锁定的“空结果”。",[14,8208,8210],{"id":8209},"十二如何选择隔离级别","十二、如何选择隔离级别",[196,8212,8214],{"id":8213},"选择-read-committed","选择 READ COMMITTED",[18,8216,3304],{},[174,8218,8219,8222,8225,8228],{},[25,8220,8221],{},"希望每次查询都看到最新已提交数据；",[25,8223,8224],{},"可以接受同一事务内两次读取结果不同；",[25,8226,8227],{},"希望减少 Gap Lock 和锁冲突；",[25,8229,8230],{},"业务通过版本号、唯一约束或其他机制保证并发正确性。",[196,8232,8234],{"id":8233},"选择-repeatable-read","选择 REPEATABLE READ",[18,8236,3304],{},[174,8238,8239,8242,8245,8248],{},[25,8240,8241],{},"希望事务中的普通查询保持一致视图；",[25,8243,8244],{},"需要使用 Next-Key Lock 保护范围；",[25,8246,8247],{},"能够控制索引、锁顺序和事务时长；",[25,8249,8250],{},"接受 InnoDB 默认隔离级别。",[196,8252,8254],{"id":8253},"选择-serializable","选择 SERIALIZABLE",[18,8256,3304],{},[174,8258,8259,8262,8265,8268],{},[25,8260,8261],{},"并发量较低；",[25,8263,8264],{},"一致性要求极高；",[25,8266,8267],{},"业务逻辑难以通过更细粒度约束实现；",[25,8269,8270],{},"能够接受更多阻塞、超时和死锁重试。",[18,8272,8273],{},"隔离级别不是越高越好。选择时需要综合评估：",[174,8275,8276,8279,8282,8285,8288,8291],{},[25,8277,8278],{},"一致性要求；",[25,8280,8281],{},"读写比例；",[25,8283,8284],{},"热点数据；",[25,8286,8287],{},"事务持续时间；",[25,8289,8290],{},"可接受的锁等待；",[25,8292,8293],{},"数据库吞吐量。",[14,8295,8297],{"id":8296},"十三查看和设置隔离级别","十三、查看和设置隔离级别",[18,8299,8300],{},"查看当前会话隔离级别：",[204,8302,8304],{"className":7483,"code":8303,"language":7485,"meta":210,"style":210},"SELECT @@SESSION.transaction_isolation;\n",[119,8305,8306],{"__ignoreMap":210},[460,8307,8308],{"class":462,"line":463},[460,8309,8303],{},[18,8311,8312],{},"查看全局默认隔离级别：",[204,8314,8316],{"className":7483,"code":8315,"language":7485,"meta":210,"style":210},"SELECT @@GLOBAL.transaction_isolation;\n",[119,8317,8318],{"__ignoreMap":210},[460,8319,8320],{"class":462,"line":463},[460,8321,8315],{},[18,8323,8324],{},"修改当前会话后续事务的隔离级别：",[204,8326,8328],{"className":7483,"code":8327,"language":7485,"meta":210,"style":210},"SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;\n",[119,8329,8330],{"__ignoreMap":210},[460,8331,8332],{"class":462,"line":463},[460,8333,8327],{},[18,8335,8336],{},"设置下一个事务的隔离级别：",[204,8338,8340],{"className":7483,"code":8339,"language":7485,"meta":210,"style":210},"SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;\n",[119,8341,8342],{"__ignoreMap":210},[460,8343,8344],{"class":462,"line":463},[460,8345,8339],{},[18,8347,8348],{},"开始事务：",[204,8350,8351],{"className":7483,"code":7594,"language":7485,"meta":210,"style":210},[119,8352,8353],{"__ignoreMap":210},[460,8354,8355],{"class":462,"line":463},[460,8356,7594],{},[18,8358,8359],{},"线上修改隔离级别前，需要：",[174,8361,8362,8365,8372],{},[25,8363,8364],{},"验证依赖一致性快照、锁定范围和“先查后写”的业务逻辑；",[25,8366,8367,8368,8371],{},"确认 ",[119,8369,8370],{},"binlog_format"," 与目标隔离级别兼容；",[25,8373,8374],{},"观察切换前后的锁等待、死锁率、事务时延和数据库吞吐量。",[18,8376,8377,8378,8381,8382,8385],{},"这里需要确认的不是笼统的“复制方式”，而是二进制日志格式的兼容性。例如 MySQL 的 READ COMMITTED 只支持 Row-Based Logging；如果配置为 ",[119,8379,8380],{},"MIXED","，MySQL 会自动使用行格式记录。系统原本已经使用 ",[119,8383,8384],{},"ROW"," 时，通常不需要为此额外调整。",[18,8387,8388],{},"监控指标也不是选择隔离级别的前置条件，而是用于验证切换产生的实际影响：",[174,8390,8391,8394],{},[25,8392,8393],{},"从 REPEATABLE READ 切换到 READ COMMITTED，Gap Lock 通常减少，锁等待和死锁概率可能下降，但业务会失去事务级固定快照；",[25,8395,8396],{},"切换到更高隔离级别时，锁范围和等待时间可能增加，事务耗时及吞吐量可能受到影响。",[18,8398,8399],{},"因此，不能只根据理论吞吐量直接切换隔离级别。",[14,8401,3467],{"id":3466},[196,8403,8405],{"id":8404},"误区-1mvcc-会阻止其他事务插入","误区 1：MVCC 会阻止其他事务插入",[18,8407,8408],{},"不会。MVCC 主要决定一条记录对当前 Read View 是否可见，不会因为快照读而阻塞其他事务写入。",[196,8410,8412],{"id":8411},"误区-2repeatable-read-只靠-mvcc-避免所有幻读","误区 2：REPEATABLE READ 只靠 MVCC 避免所有幻读",[18,8414,8415],{},"不完整。",[174,8417,8418,8421],{},[25,8419,8420],{},"快照读靠 MVCC 和固定 Read View；",[25,8422,8423],{},"当前读靠 Gap Lock 和 Next-Key Lock。",[196,8425,8427],{"id":8426},"误区-3只要加-for-update-就一定不会幻读","误区 3：只要加 FOR UPDATE 就一定不会幻读",[18,8429,8430],{},"不一定。是否锁住间隙与隔离级别、查询条件、索引类型和实际扫描范围有关。READ COMMITTED 下通常不会为普通范围查询使用 Gap Lock。",[196,8432,8434],{"id":8433},"误区-4next-key-lock-锁的是数据页","误区 4：Next-Key Lock 锁的是数据页",[18,8436,8437],{},"不是。它围绕索引记录和索引记录之间的间隙建立。",[196,8439,8441],{"id":8440},"误区-5没有索引就一定升级为表锁","误区 5：没有索引就一定升级为表锁",[18,8443,8444],{},"这个表述不准确。InnoDB 仍然基于索引进行记录锁定，但缺少合适索引可能导致扫描和锁定大量记录，使并发效果接近大范围锁定。",[196,8446,8448],{"id":8447},"误区-6serializable-一定是最好的选择","误区 6：SERIALIZABLE 一定是最好的选择",[18,8450,8451],{},"它的隔离性最强，但会显著增加锁等待，降低并发能力。多数业务更适合通过合理隔离级别、索引、锁定读和数据库约束共同保证正确性。",[14,8453,1092],{"id":1092},[174,8455,8456,8463,8470,8477,8484],{},[25,8457,8458],{},[70,8459,8462],{"href":8460,"rel":8461},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-transaction-isolation-levels.html",[1101],"MySQL 8.4 Reference Manual：Transaction Isolation Levels",[25,8464,8465],{},[70,8466,8469],{"href":8467,"rel":8468},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-consistent-read.html",[1101],"MySQL 8.4 Reference Manual：Consistent Nonlocking Reads",[25,8471,8472],{},[70,8473,8476],{"href":8474,"rel":8475},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-next-key-locking.html",[1101],"MySQL 8.4 Reference Manual：Phantom Rows",[25,8478,8479],{},[70,8480,8483],{"href":8481,"rel":8482},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-locking-reads.html",[1101],"MySQL 8.4 Reference Manual：Locking Reads",[25,8485,8486],{},[70,8487,8490],{"href":8488,"rel":8489},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Fset-transaction.html",[1101],"MySQL 8.4 Reference Manual：SET TRANSACTION Statement",[1146,8492,1148],{},{"title":210,"searchDepth":469,"depth":469,"links":8494},[8495,8498,8499,8500,8501,8506,8512,8516,8517,8518,8523,8524,8525,8526,8527,8532,8533,8541],{"id":16,"depth":469,"text":16,"children":8496},[8497],{"id":7181,"depth":1158,"text":7181},{"id":66,"depth":469,"text":66},{"id":83,"depth":469,"text":83},{"id":7202,"depth":469,"text":7203},{"id":7308,"depth":469,"text":7309,"children":8502},[8503,8504,8505],{"id":7312,"depth":1158,"text":7313},{"id":7331,"depth":1158,"text":7332},{"id":7352,"depth":1158,"text":7353},{"id":7387,"depth":469,"text":7388,"children":8507},[8508,8509,8510,8511],{"id":7391,"depth":1158,"text":7392},{"id":7412,"depth":1158,"text":7413},{"id":7445,"depth":1158,"text":7446},{"id":7470,"depth":1158,"text":7471},{"id":7500,"depth":469,"text":7501,"children":8513},[8514,8515],{"id":7507,"depth":1158,"text":7508},{"id":7543,"depth":1158,"text":7544},{"id":7580,"depth":469,"text":7581},{"id":7672,"depth":469,"text":7673},{"id":7756,"depth":469,"text":7757,"children":8519},[8520,8521,8522],{"id":7760,"depth":1158,"text":7761},{"id":7793,"depth":1158,"text":7794},{"id":7821,"depth":1158,"text":7822},{"id":7987,"depth":469,"text":7988},{"id":8055,"depth":469,"text":8056},{"id":8126,"depth":469,"text":8127},{"id":8166,"depth":469,"text":8167},{"id":8209,"depth":469,"text":8210,"children":8528},[8529,8530,8531],{"id":8213,"depth":1158,"text":8214},{"id":8233,"depth":1158,"text":8234},{"id":8253,"depth":1158,"text":8254},{"id":8296,"depth":469,"text":8297},{"id":3466,"depth":469,"text":3467,"children":8534},[8535,8536,8537,8538,8539,8540],{"id":8404,"depth":1158,"text":8405},{"id":8411,"depth":1158,"text":8412},{"id":8426,"depth":1158,"text":8427},{"id":8433,"depth":1158,"text":8434},{"id":8440,"depth":1158,"text":8441},{"id":8447,"depth":1158,"text":8448},{"id":1092,"depth":469,"text":1092},"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":7112,"description":8543},"java\u002Fmysql-transaction-isolation-and-phantom-read","sTjUvqr3aDsCis_rj0ygbjCxuvyf9jFgLpnBBrvGgZw",{"id":8551,"title":8552,"body":8553,"commentId":9585,"description":9586,"difficulty":2636,"draft":1208,"extension":1209,"meta":9587,"navigation":1211,"order":9588,"path":9589,"section":1214,"seo":9590,"stem":9591,"updated":9592,"__hash__":9593},"java\u002Fjava\u002Fmysql-bplus-tree-vs-btree.md","MySQL 为何采用 B+ 树？B+ 树与 B 树的区别是什么？",{"type":7,"value":8554,"toc":9554},[8555,8557,8560,8563,8577,8580,8583,8585,8590,8592,8601,8603,8607,8614,8634,8637,8639,8653,8657,8660,8663,8666,8672,8675,8692,8699,8703,8706,8709,8715,8718,8734,8737,8751,8755,8859,8862,8865,8869,8872,8875,8878,8884,8887,8894,8899,8903,8908,8911,8916,8919,8925,8928,8934,8937,8943,8946,8950,8953,8956,8962,8965,8968,8971,8977,8980,8986,8989,8994,9003,9007,9010,9030,9037,9043,9046,9052,9055,9058,9103,9106,9110,9113,9116,9122,9125,9132,9136,9139,9147,9150,9153,9178,9181,9184,9201,9204,9207,9211,9214,9217,9232,9235,9241,9244,9253,9256,9259,9263,9266,9272,9275,9284,9287,9307,9312,9318,9321,9324,9342,9345,9354,9358,9361,9364,9370,9373,9376,9380,9383,9400,9403,9407,9410,9413,9419,9422,9425,9439,9442,9446,9450,9453,9457,9460,9464,9467,9471,9474,9478,9489,9493,9496,9502,9508,9513,9515,9552],[14,8556,16],{"id":16},[18,8558,8559],{},"MySQL InnoDB 的普通索引采用通常所说的 B+ 树结构，核心原因是数据库索引需要同时兼顾磁盘或页访问次数、等值查询、范围查询和顺序遍历。",[18,8561,8562],{},"B+ 树与 B 树的主要区别是：",[174,8564,8565,8568,8571,8574],{},[25,8566,8567],{},"B 树的非叶子节点和叶子节点都可以保存数据记录；",[25,8569,8570],{},"B+ 树的非叶子节点主要保存索引键和子页面指针，完整索引记录集中在叶子节点；",[25,8572,8573],{},"B+ 树的叶子节点按照键值顺序连接，适合范围扫描；",[25,8575,8576],{},"由于非叶子节点不保存完整数据，同样大小的页面能够容纳更多目录项，树的扇出更大、高度更低。",[18,8578,8579],{},"InnoDB 默认索引页大小为 16KB。一次读取通常以页为单位，而不是只读取一个键。B+ 树通过较大的扇出，把大量数据组织在较少的层级中，查询通常只需要访问少量页面；定位到范围起点后，还可以沿叶子页顺序扫描，不必反复从根节点查找。",[18,8581,8582],{},"InnoDB 的聚簇索引叶子节点保存完整行数据，二级索引叶子节点保存二级索引键和主键值。通过二级索引查询非覆盖列时，需要先取得主键，再回到聚簇索引查找完整行，这就是常说的回表。",[196,8584,7181],{"id":7181},[57,8586,8587],{},[18,8588,8589],{},"B+ 树让非叶子节点专注于导航，以更大的扇出降低树高，并把有序数据集中在相连的叶子节点中，因此既能减少页面访问，又适合范围查询和顺序扫描。",[14,8591,66],{"id":66},[18,8593,8594],{},[70,8595,8598],{"href":8596,"target":73,"rel":8597},"\u002Fimages\u002Fwiki\u002Fjava\u002Fmysql-btree-vs-bplus-tree.svg",[75,76],[78,8599],{"src":8596,"alt":8600},"B 树与 B+ 树结构对比",[14,8602,83],{"id":83},[14,8604,8606],{"id":8605},"一先明确mysql-文档中的-b-tree-与-b-树","一、先明确：MySQL 文档中的 B-tree 与 B+ 树",[18,8608,8609,8610,8613],{},"MySQL 官方文档通常把 InnoDB 的普通索引统称为 ",[119,8611,8612],{},"B-tree"," 索引。在数据库实现和面试语境中，InnoDB 的这种索引组织通常被称为 B+ 树：",[174,8615,8616,8619,8622,8625,8628,8631],{},[25,8617,8618],{},"索引记录存放在叶子页；",[25,8620,8621],{},"非叶子页负责索引导航；",[25,8623,8624],{},"聚簇索引的叶子页保存行数据；",[25,8626,8627],{},"二级索引的叶子记录保存索引列和主键；",[25,8629,8630],{},"InnoDB 同一层的索引页通过前后页指针双向连接；",[25,8632,8633],{},"叶子页中的索引记录有序，能够进行范围扫描。",[18,8635,8636],{},"因此，“MySQL 使用 B+ 树”通常是在描述 InnoDB 普通索引的具体组织特征；它不代表 MySQL 中所有索引都使用 B+ 树。",[18,8638,666],{},[174,8640,8641,8644,8647,8650],{},[25,8642,8643],{},"InnoDB 普通索引使用 B-tree 类结构；",[25,8645,8646],{},"空间索引使用 R-tree；",[25,8648,8649],{},"全文索引有独立的倒排索引设计；",[25,8651,8652],{},"InnoDB 还可能使用自适应哈希索引加速部分热点等值访问。",[14,8654,8656],{"id":8655},"二b-树是什么","二、B 树是什么",[18,8658,8659],{},"B 树是一种多路平衡查找树。",[18,8661,8662],{},"“多路”表示一个节点可以拥有多个子节点，而不是像二叉查找树那样最多只有两个子节点。",[18,8664,8665],{},"一个简化的 B 树可以表示为：",[204,8667,8670],{"className":8668,"code":8669,"language":209,"meta":210},[207],"                 [20 | 50]\n                \u002F    |    \\\n       [5 | 10]   [30 | 40]   [60 | 80]\n",[119,8671,8669],{"__ignoreMap":210},[18,8673,8674],{},"B 树具有以下特点：",[174,8676,8677,8680,8683,8686,8689],{},[25,8678,8679],{},"节点中的键有序；",[25,8681,8682],{},"一个节点可以保存多个键和数据记录；",[25,8684,8685],{},"非叶子节点也可以直接保存数据；",[25,8687,8688],{},"所有叶子节点通常处于同一层；",[25,8690,8691],{},"插入和删除后通过分裂、合并或旋转维持平衡。",[18,8693,8694,8695,8698],{},"查询 ",[119,8696,8697],{},"20"," 时，可能在根节点直接找到数据，不一定需要访问叶子节点。",[14,8700,8702],{"id":8701},"三b-树是什么","三、B+ 树是什么",[18,8704,8705],{},"B+ 树也是多路平衡查找树，但它把导航结构和数据记录进一步分离。",[18,8707,8708],{},"一个简化的 B+ 树可以表示为：",[204,8710,8713],{"className":8711,"code":8712,"language":209,"meta":210},[207],"                         [20 | 50]\n                        \u002F    |    \\\n                       \u002F     |     \\\n        [5 | 10 | 20] [30 | 40 | 50] [60 | 80]\n                ↔              ↔\n          叶子页通过前后页指针双向连接\n",[119,8714,8712],{"__ignoreMap":210},[18,8716,8717],{},"B+ 树具有以下典型特点：",[174,8719,8720,8723,8726,8729,8731],{},[25,8721,8722],{},"非叶子节点主要保存键和子节点指针；",[25,8724,8725],{},"完整的数据记录集中在叶子节点；",[25,8727,8728],{},"所有查询最终都会到达叶子节点；",[25,8730,8630],{},[25,8732,8733],{},"适合从某个键开始连续读取一段数据。",[18,8735,8736],{},"需要注意，示意图中的分隔键可能同时出现在非叶子节点和叶子节点中。非叶子节点中的键用于导航，叶子节点中的索引记录才承载最终数据定位信息。",[18,8738,8739,8740,8743,8744,892,8747,8750],{},"这里的双向链表连接的是",[28,8741,8742],{},"索引页","，并不是说整棵 B+ 树本身就是双向链表。整体结构仍然是一棵多路平衡树：树结构负责快速定位目标页；页面头中的 ",[119,8745,8746],{},"FIL_PAGE_PREV",[119,8748,8749],{},"FIL_PAGE_NEXT"," 字段保存前后页的页号，这些页号相当于逻辑指针，用于前后遍历。单个索引页内部的记录则主要按键值顺序组成单向链表，并由页目录辅助查找。",[14,8752,8754],{"id":8753},"四b-树与-b-树的核心区别","四、B 树与 B+ 树的核心区别",[92,8756,8757,8769],{},[95,8758,8759],{},[98,8760,8761,8763,8766],{},[101,8762,2756],{},[101,8764,8765],{},"B 树",[101,8767,8768],{},"B+ 树",[111,8770,8771,8782,8793,8804,8815,8826,8837,8848],{},[98,8772,8773,8776,8779],{},[116,8774,8775],{},"数据记录位置",[116,8777,8778],{},"非叶子节点和叶子节点都可以保存",[116,8780,8781],{},"完整索引记录集中在叶子节点",[98,8783,8784,8787,8790],{},[116,8785,8786],{},"非叶子节点职责",[116,8788,8789],{},"导航，同时可能保存数据",[116,8791,8792],{},"主要负责导航",[98,8794,8795,8798,8801],{},[116,8796,8797],{},"单个节点可容纳的键",[116,8799,8800],{},"相对较少",[116,8802,8803],{},"通常更多",[98,8805,8806,8809,8812],{},[116,8807,8808],{},"树的扇出",[116,8810,8811],{},"相对较小",[116,8813,8814],{},"通常更大",[98,8816,8817,8820,8823],{},[116,8818,8819],{},"树高",[116,8821,8822],{},"相同数据量下可能更高",[116,8824,8825],{},"相同数据量下通常更低",[98,8827,8828,8831,8834],{},[116,8829,8830],{},"等值查询",[116,8832,8833],{},"可能在非叶子节点提前结束",[116,8835,8836],{},"通常需要走到叶子节点",[98,8838,8839,8842,8845],{},[116,8840,8841],{},"范围查询",[116,8843,8844],{},"可能需要在树中多次定位",[116,8846,8847],{},"定位起点后可顺序扫描叶子节点",[98,8849,8850,8853,8856],{},[116,8851,8852],{},"查询路径",[116,8854,8855],{},"不同记录的结束层级可能不同",[116,8857,8858],{},"通常都到叶子层，路径更稳定",[18,8860,8861],{},"不能简单得出“B+ 树在所有场景都比 B 树快”的结论。",[18,8863,8864],{},"如果整个数据结构都在内存中，而且只做等值查询，B 树可能在非叶子节点提前命中。但数据库索引更关心页面访问、范围扫描和批量读取，B+ 树的整体特性更适合这类负载。",[14,8866,8868],{"id":8867},"五为什么数据库关心页面访问次数","五、为什么数据库关心页面访问次数",[18,8870,8871],{},"InnoDB 管理数据的基本单位是页，默认页面大小为 16KB。",[18,8873,8874],{},"查询一个索引键时，底层不是只从磁盘读取这个键对应的几个字节，而是把相关页面读入 Buffer Pool，再在页内查找。",[18,8876,8877],{},"因此，索引设计要尽量做到：",[204,8879,8882],{"className":8880,"code":8881,"language":209,"meta":210},[207],"更少的树层级\n        ↓\n更少的页面访问\n        ↓\n更低的 I\u002FO 和缓存查找成本\n",[119,8883,8881],{"__ignoreMap":210},[18,8885,8886],{},"即使相关页面已经在 Buffer Pool 中，查询仍然需要逐层定位索引页、在页内比较目录项，并在必要时获取页面 Latch。树高较低通常意味着遍历的页面更少，但此时节省的主要是内存中的索引导航成本，而不是磁盘 I\u002FO。",[18,8888,8889,8890,8893],{},"所以分析 B+ 树时，不应只背诵时间复杂度 ",[119,8891,8892],{},"O(log N)","，还要关注：",[57,8895,8896],{},[18,8897,8898],{},"这个对数以多大的扇出为底，以及一次查询需要访问多少个页面。",[14,8900,8902],{"id":8901},"六扇出是什么","六、扇出是什么",[18,8904,8905],{},[28,8906,8907],{},"扇出（Fan-out）是一个非叶子节点能够直接指向的子节点数量。",[18,8909,8910],{},"在 InnoDB 中，可以把它理解为：",[57,8912,8913],{},[18,8914,8915],{},"一个非叶子索引页能够直接导航到多少个下层索引页。",[18,8917,8918],{},"例如，某个非叶子页保存了大量“索引值 + 子页面指针”目录项，并能够据此导航到约 1,000 个子页面，那么可以近似地说这个节点的扇出约为 1,000。",[18,8920,8921,8922,326],{},"扇出不是整棵树的记录总数，也不是一个叶子页能够保存的行数。它描述的是",[28,8923,8924],{},"单个非叶子节点向下一层分出的分支数量",[18,8926,8927],{},"对于相同数量的数据，树高可以粗略理解为：",[204,8929,8932],{"className":8930,"code":8931,"language":209,"meta":210},[207],"树高 ≈ log(扇出)(需要管理的页面数)\n",[119,8933,8931],{"__ignoreMap":210},[18,8935,8936],{},"扇出越大，每增加一层能够覆盖的页面数增长得越快，因此管理相同数据量所需的层级通常越少：",[204,8938,8941],{"className":8939,"code":8940,"language":209,"meta":210},[207],"扇出为 2：       2 → 4 → 8 → 16\n扇出为 1,000：   1,000 → 1,000,000 → 1,000,000,000\n",[119,8942,8940],{"__ignoreMap":210},[18,8944,8945],{},"这里的数字只用于说明增长速度。真实扇出会受到索引键长度、页面格式、记录头、页面填充率等因素影响。",[14,8947,8949],{"id":8948},"七b-树如何通过更大的扇出降低树高","七、B+ 树如何通过更大的扇出降低树高",[18,8951,8952],{},"假设一个非叶子页既保存键，又保存完整行数据，那么每个目录项会比较大，一个页面能够容纳的目录项就比较少。",[18,8954,8955],{},"如果非叶子页只保存：",[204,8957,8960],{"className":8958,"code":8959,"language":209,"meta":210},[207],"索引键 + 子页面指针\n",[119,8961,8959],{"__ignoreMap":210},[18,8963,8964],{},"每个目录项更小，同一个 16KB 页面就能容纳更多目录项，一个节点可以指向更多子页面。",[18,8966,8967],{},"由于单个目录项更紧凑，同一个页面可以保存更多目录项、指向更多子页面，这就是 B+ 树扇出更大的原因。",[18,8969,8970],{},"例如，仅用于理解数量级，假设一个非叶子目录项平均占用 16 字节，忽略页头、槽目录、填充率和其他开销：",[204,8972,8975],{"className":8973,"code":8974,"language":209,"meta":210},[207],"16KB ÷ 16B ≈ 1024\n",[119,8976,8974],{"__ignoreMap":210},[18,8978,8979],{},"一个非叶子页理论上可以导航约一千个子页面：",[204,8981,8984],{"className":8982,"code":8983,"language":209,"meta":210},[207],"根节点：约 1,000 个分支\n第二层：约 1,000 × 1,000 个分支\n第三层：继续扩大\n",[119,8985,8983],{"__ignoreMap":210},[18,8987,8988],{},"真实容量会受到键长度、页格式、记录头、填充率等因素影响，不能直接按这个示例计算生产容量。但它能说明：",[57,8990,8991],{},[18,8992,8993],{},"多路树的扇出很大，少量层级就可以管理大量记录。",[18,8995,8996],{},[70,8997,9000],{"href":8998,"target":73,"rel":8999},"\u002Fimages\u002Fwiki\u002Fjava\u002Fmysql-bplus-tree-fanout.svg",[75,76],[78,9001],{"src":8998,"alt":9002},"B+ 树通过大扇出降低树高",[14,9004,9006],{"id":9005},"八为什么-b-树适合范围查询","八、为什么 B+ 树适合范围查询",[18,9008,9009],{},"假设执行：",[204,9011,9013],{"className":7483,"code":9012,"language":7485,"meta":210,"style":210},"SELECT *\nFROM orders\nWHERE id BETWEEN 1000 AND 2000;\n",[119,9014,9015,9020,9025],{"__ignoreMap":210},[460,9016,9017],{"class":462,"line":463},[460,9018,9019],{},"SELECT *\n",[460,9021,9022],{"class":462,"line":469},[460,9023,9024],{},"FROM orders\n",[460,9026,9027],{"class":462,"line":1158},[460,9028,9029],{},"WHERE id BETWEEN 1000 AND 2000;\n",[18,9031,9032,9033,9036],{},"B+ 树可以先从根节点定位到 ",[119,9034,9035],{},"id=1000"," 附近的叶子页：",[204,9038,9041],{"className":9039,"code":9040,"language":209,"meta":210},[207],"根页\n  ↓\n非叶子页\n  ↓\n包含 1000 的叶子页\n",[119,9042,9040],{"__ignoreMap":210},[18,9044,9045],{},"定位范围起点以后，可以沿叶子页顺序扫描：",[204,9047,9050],{"className":9048,"code":9049,"language":209,"meta":210},[207],"[1000 ... 1200] ↔ [1201 ... 1500] ↔ [1501 ... 1800] ↔ [1801 ... 2000]\n",[119,9051,9049],{"__ignoreMap":210},[18,9053,9054],{},"不需要为范围中的每一条记录重新从根节点查找。",[18,9056,9057],{},"因此，B+ 树天然适合：",[174,9059,9060,9074,9079,9085,9091,9100],{},[25,9061,9062,892,9065,892,9068,892,9071,1426],{},[119,9063,9064],{},">",[119,9066,9067],{},">=",[119,9069,9070],{},"\u003C",[119,9072,9073],{},"\u003C=",[25,9075,9076,1426],{},[119,9077,9078],{},"BETWEEN",[25,9080,9081,9082,1426],{},"有效前缀的 ",[119,9083,9084],{},"LIKE 'abc%'",[25,9086,9087,9088,1426],{},"按索引顺序执行的 ",[119,9089,9090],{},"ORDER BY",[25,9092,9093,9094,892,9097,1426],{},"部分 ",[119,9095,9096],{},"MIN",[119,9098,9099],{},"MAX",[25,9101,9102],{},"范围扫描和顺序分页。",[18,9104,9105],{},"能否真正使用索引顺序，还取决于联合索引结构、最左前缀、排序方向和执行计划，不能因为底层是 B+ 树就认为所有范围条件都会高效。",[14,9107,9109],{"id":9108},"九为什么不用二叉查找树或红黑树","九、为什么不用二叉查找树或红黑树",[18,9111,9112],{},"二叉查找树和红黑树的扇出通常是 2。",[18,9114,9115],{},"即使红黑树能够保持近似平衡，管理大量数据时树高仍然明显高于多路树：",[204,9117,9120],{"className":9118,"code":9119,"language":209,"meta":210},[207],"二叉树：\n每个节点最多 2 个分支\n\nB+ 树：\n每个页面可以拥有数百甚至更多分支\n",[119,9121,9119],{"__ignoreMap":210},[18,9123,9124],{},"如果每层节点可能位于不同页面，树越高，查询可能需要访问的页面越多。",[18,9126,9127,9128,9131],{},"红黑树很适合内存中的集合和映射，例如 Java 的 ",[119,9129,9130],{},"TreeMap","；数据库索引需要尽量减少页面 I\u002FO，并支持大范围顺序扫描，因此多路 B+ 树更加合适。",[14,9133,9135],{"id":9134},"十为什么不直接使用哈希索引","十、为什么不直接使用哈希索引",[18,9137,9138],{},"哈希结构擅长等值查询：",[204,9140,9141],{"className":7483,"code":7780,"language":7485,"meta":210,"style":210},[119,9142,9143],{"__ignoreMap":210},[460,9144,9145],{"class":462,"line":463},[460,9146,7780],{},[18,9148,9149],{},"理想情况下可以根据哈希值直接定位桶。",[18,9151,9152],{},"但哈希值不保留原始键的顺序，因此不适合：",[204,9154,9156],{"className":7483,"code":9155,"language":7485,"meta":210,"style":210},"WHERE id > 100;\nWHERE id BETWEEN 100 AND 200;\nORDER BY id;\nLIKE 'abc%';\n",[119,9157,9158,9163,9168,9173],{"__ignoreMap":210},[460,9159,9160],{"class":462,"line":463},[460,9161,9162],{},"WHERE id > 100;\n",[460,9164,9165],{"class":462,"line":469},[460,9166,9167],{},"WHERE id BETWEEN 100 AND 200;\n",[460,9169,9170],{"class":462,"line":1158},[460,9171,9172],{},"ORDER BY id;\n",[460,9174,9175],{"class":462,"line":2373},[460,9176,9177],{},"LIKE 'abc%';\n",[18,9179,9180],{},"哈希索引还需要处理哈希冲突，也无法自然支持联合索引的有序最左前缀扫描。",[18,9182,9183],{},"B+ 树虽然等值查找通常需要沿树下降，但同时支持：",[174,9185,9186,9189,9192,9195,9198],{},[25,9187,9188],{},"等值查询；",[25,9190,9191],{},"范围查询；",[25,9193,9194],{},"顺序扫描；",[25,9196,9197],{},"排序；",[25,9199,9200],{},"联合索引前缀匹配。",[18,9202,9203],{},"因此它的通用性更强。",[18,9205,9206],{},"InnoDB 的自适应哈希索引可以为部分热点 B-tree 页面建立哈希访问路径，但它属于内部优化，不会替代原有 B+ 树索引。",[14,9208,9210],{"id":9209},"十一innodb-聚簇索引如何组织数据","十一、InnoDB 聚簇索引如何组织数据",[18,9212,9213],{},"每张 InnoDB 表都有一个聚簇索引。",[18,9215,9216],{},"聚簇索引的选择顺序通常是：",[22,9218,9219,9222,9229],{},[25,9220,9221],{},"显式定义的主键；",[25,9223,9224,9225,9228],{},"第一个所有列均为 ",[119,9226,9227],{},"NOT NULL"," 的唯一索引；",[25,9230,9231],{},"InnoDB 自动生成的隐藏行 ID。",[18,9233,9234],{},"聚簇索引可以简化为：",[204,9236,9239],{"className":9237,"code":9238,"language":209,"meta":210},[207],"非叶子节点：\n主键 + 子页面指针\n\n叶子节点：\n主键 + 完整行数据\n",[119,9240,9238],{"__ignoreMap":210},[18,9242,9243],{},"因此，通过主键查询：",[204,9245,9247],{"className":7483,"code":9246,"language":7485,"meta":210,"style":210},"SELECT * FROM users WHERE id = 100;\n",[119,9248,9249],{"__ignoreMap":210},[460,9250,9251],{"class":462,"line":463},[460,9252,9246],{},[18,9254,9255],{},"沿聚簇索引找到叶子记录时，通常已经取得了完整行数据。",[18,9257,9258],{},"这也是为什么官方文档把 InnoDB 表称为按聚簇索引组织的数据结构。",[14,9260,9262],{"id":9261},"十二innodb-二级索引如何组织数据","十二、InnoDB 二级索引如何组织数据",[18,9264,9265],{},"二级索引的叶子记录不直接保存完整行，而是保存：",[204,9267,9270],{"className":9268,"code":9269,"language":209,"meta":210},[207],"二级索引列 + 主键值\n",[119,9271,9269],{"__ignoreMap":210},[18,9273,9274],{},"假设存在：",[204,9276,9278],{"className":7483,"code":9277,"language":7485,"meta":210,"style":210},"CREATE INDEX idx_users_name ON users(name);\n",[119,9279,9280],{"__ignoreMap":210},[460,9281,9282],{"class":462,"line":463},[460,9283,9277],{},[18,9285,9286],{},"执行：",[204,9288,9290],{"className":7483,"code":9289,"language":7485,"meta":210,"style":210},"SELECT age\nFROM users\nWHERE name = 'Tom';\n",[119,9291,9292,9297,9302],{"__ignoreMap":210},[460,9293,9294],{"class":462,"line":463},[460,9295,9296],{},"SELECT age\n",[460,9298,9299],{"class":462,"line":469},[460,9300,9301],{},"FROM users\n",[460,9303,9304],{"class":462,"line":1158},[460,9305,9306],{},"WHERE name = 'Tom';\n",[18,9308,3234,9309,9311],{},[119,9310,1623],{}," 不在二级索引中，查询过程通常是：",[204,9313,9316],{"className":9314,"code":9315,"language":209,"meta":210},[207],"idx_users_name B+ 树\n        ↓\n找到 name='Tom' 对应的主键\n        ↓\n回到聚簇索引 B+ 树\n        ↓\n根据主键取得完整行和 age\n",[119,9317,9315],{"__ignoreMap":210},[18,9319,9320],{},"这就是回表。",[18,9322,9323],{},"如果查询所需列都能从二级索引叶子记录中取得：",[204,9325,9327],{"className":7483,"code":9326,"language":7485,"meta":210,"style":210},"SELECT id\nFROM users\nWHERE name = 'Tom';\n",[119,9328,9329,9334,9338],{"__ignoreMap":210},[460,9330,9331],{"class":462,"line":463},[460,9332,9333],{},"SELECT id\n",[460,9335,9336],{"class":462,"line":469},[460,9337,9301],{},[460,9339,9340],{"class":462,"line":1158},[460,9341,9306],{},[18,9343,9344],{},"那么可能直接通过二级索引完成查询，不需要回到聚簇索引，这就是覆盖索引。",[18,9346,9347],{},[70,9348,9351],{"href":9349,"target":73,"rel":9350},"\u002Fimages\u002Fwiki\u002Fjava\u002Fmysql-innodb-index-lookup.svg",[75,76],[78,9352],{"src":9349,"alt":9353},"InnoDB 聚簇索引、二级索引与回表",[14,9355,9357],{"id":9356},"十三为什么主键不宜过长","十三、为什么主键不宜过长",[18,9359,9360],{},"InnoDB 的每条二级索引记录都包含主键值。",[18,9362,9363],{},"如果主键很长：",[204,9365,9368],{"className":9366,"code":9367,"language":209,"meta":210},[207],"每条二级索引记录更大\n        ↓\n一个索引页容纳的记录更少\n        ↓\n索引占用空间增加\n        ↓\n缓存命中率和扇出可能下降\n",[119,9369,9367],{"__ignoreMap":210},[18,9371,9372],{},"因此，在满足业务要求的前提下，较短且稳定的主键通常更有利于 InnoDB 索引组织。",[18,9374,9375],{},"这并不意味着所有表都必须使用自增整数主键。是否采用自增 ID、分布式 ID 或业务主键，还要考虑写入热点、分库分表、数据合并和业务语义。",[14,9377,9379],{"id":9378},"十四叶子节点有序带来的其他收益","十四、叶子节点有序带来的其他收益",[18,9381,9382],{},"除了范围查询，叶子节点按键值有序还可以帮助：",[174,9384,9385,9388,9391,9394,9397],{},[25,9386,9387],{},"按索引顺序输出数据，减少额外排序；",[25,9389,9390],{},"快速定位最小值和最大值；",[25,9392,9393],{},"连续读取相邻索引记录；",[25,9395,9396],{},"对联合索引执行最左前缀扫描；",[25,9398,9399],{},"利用页预读和 Buffer Pool 提高连续访问效率。",[18,9401,9402],{},"但“逻辑有序”不等于“所有叶子页在磁盘上永远物理连续”。页面分裂、删除、合并和长期更新都会产生碎片，同层索引页主要依靠页面头中保存的前后页页号维持逻辑顺序。这些页号发挥逻辑指针的作用，并不是进程内存地址意义上的裸指针。",[14,9404,9406],{"id":9405},"十五b-树的写入代价","十五、B+ 树的写入代价",[18,9408,9409],{},"B+ 树并不是只有优点。",[18,9411,9412],{},"插入新记录时，需要先找到目标叶子页。如果页面空间不足，可能发生页分裂：",[204,9414,9417],{"className":9415,"code":9416,"language":209,"meta":210},[207],"一个满页\n   ↓\n分裂为两个页\n   ↓\n更新父节点目录项\n",[119,9418,9416],{"__ignoreMap":210},[18,9420,9421],{},"删除大量记录后，也可能触发页面合并或树结构收缩。",[18,9423,9424],{},"随机主键写入容易把数据分散到不同叶子页，可能带来：",[174,9426,9427,9430,9433,9436],{},[25,9428,9429],{},"更多随机页面访问；",[25,9431,9432],{},"页分裂；",[25,9434,9435],{},"页面利用率下降；",[25,9437,9438],{},"Buffer Pool 压力增加。",[18,9440,9441],{},"顺序递增键通常更容易写入索引右侧页面，但高并发场景下也可能形成右侧热点。主键选择需要结合实际写入模式评估。",[14,9443,9445],{"id":9444},"十六常见误区","十六、常见误区",[196,9447,9449],{"id":9448},"误区-1mysql-所有索引都是-b-树","误区 1：MySQL 所有索引都是 B+ 树",[18,9451,9452],{},"不准确。这里主要讨论 InnoDB 的普通索引。空间索引、全文索引和内部自适应哈希索引采用不同结构或机制。",[196,9454,9456],{"id":9455},"误区-2b-树的所有数据都只存在叶子节点","误区 2：B+ 树的所有数据都只存在叶子节点",[18,9458,9459],{},"需要区分“完整索引记录”和“分隔键”。非叶子节点也会保存用于导航的键，但完整索引记录集中在叶子节点。",[196,9461,9463],{"id":9462},"误区-3b-树一定比-b-树快","误区 3：B+ 树一定比 B 树快",[18,9465,9466],{},"不一定。纯内存、单次等值查询中，B 树可能提前在非叶子节点命中。B+ 树的优势主要体现在页面扇出、稳定查询路径和范围扫描。",[196,9468,9470],{"id":9469},"误区-4使用-b-树后查询一定只需要三次-io","误区 4：使用 B+ 树后查询一定只需要三次 I\u002FO",[18,9472,9473],{},"树高不是固定值，取决于页面大小、键长度、记录大小、填充率和数据量。页面还可能已经位于 Buffer Pool 中，因此树层访问次数也不能直接等同于磁盘 I\u002FO 次数。",[196,9475,9477],{"id":9476},"误区-5建立索引就一定能提高性能","误区 5：建立索引就一定能提高性能",[18,9479,9480,9481,892,9484,892,9486,9488],{},"索引会占用空间并增加 ",[119,9482,9483],{},"INSERT",[119,9485,7148],{},[119,9487,7151],{}," 的维护成本。低选择性索引、无法满足最左前缀的查询或返回大量数据的查询，也可能不使用索引。",[14,9490,9492],{"id":9491},"十七回答时可以怎样组织","十七、回答时可以怎样组织",[18,9494,9495],{},"面试时可以按照以下顺序回答：",[204,9497,9500],{"className":9498,"code":9499,"language":209,"meta":210},[207],"先说结构差异\n    ↓\n再说数据库按页读取\n    ↓\n解释扇出更大、树高更低\n    ↓\n解释叶子节点有序，适合范围扫描\n    ↓\n结合 InnoDB 聚簇索引和二级索引\n",[119,9501,9499],{"__ignoreMap":210},[18,9503,9504,9505,9507],{},"重点不是只说“B+ 树查询复杂度是 ",[119,9506,8892],{},"”，而是说明：",[57,9509,9510],{},[18,9511,9512],{},"InnoDB 通过页组织索引，B+ 树让一个非叶子页保存更多导航项，从而用较少层级管理大量数据；定位到叶子页后，又能沿有序叶子页完成范围扫描。",[14,9514,1092],{"id":1092},[174,9516,9517,9524,9531,9538,9545],{},[25,9518,9519],{},[70,9520,9523],{"href":9521,"rel":9522},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-index-types.html",[1101],"MySQL 8.4 Reference Manual：Clustered and Secondary Indexes",[25,9525,9526],{},[70,9527,9530],{"href":9528,"rel":9529},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-physical-structure.html",[1101],"MySQL 8.4 Reference Manual：The Physical Structure of an InnoDB Index",[25,9532,9533],{},[70,9534,9537],{"href":9535,"rel":9536},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Fcolumn-indexes.html",[1101],"MySQL 8.4 Reference Manual：Column Indexes",[25,9539,9540],{},[70,9541,9544],{"href":9542,"rel":9543},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Findex-btree-hash.html",[1101],"MySQL 8.4 Reference Manual：Comparison of B-Tree and Hash Indexes",[25,9546,9547],{},[70,9548,9551],{"href":9549,"rel":9550},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.4\u002Fen\u002Finnodb-file-space.html",[1101],"MySQL 8.4 Reference Manual：File Space Management",[1146,9553,1148],{},{"title":210,"searchDepth":469,"depth":469,"links":9555},[9556,9559,9560,9561,9562,9563,9564,9565,9566,9567,9568,9569,9570,9571,9572,9573,9574,9575,9576,9583,9584],{"id":16,"depth":469,"text":16,"children":9557},[9558],{"id":7181,"depth":1158,"text":7181},{"id":66,"depth":469,"text":66},{"id":83,"depth":469,"text":83},{"id":8605,"depth":469,"text":8606},{"id":8655,"depth":469,"text":8656},{"id":8701,"depth":469,"text":8702},{"id":8753,"depth":469,"text":8754},{"id":8867,"depth":469,"text":8868},{"id":8901,"depth":469,"text":8902},{"id":8948,"depth":469,"text":8949},{"id":9005,"depth":469,"text":9006},{"id":9108,"depth":469,"text":9109},{"id":9134,"depth":469,"text":9135},{"id":9209,"depth":469,"text":9210},{"id":9261,"depth":469,"text":9262},{"id":9356,"depth":469,"text":9357},{"id":9378,"depth":469,"text":9379},{"id":9405,"depth":469,"text":9406},{"id":9444,"depth":469,"text":9445,"children":9577},[9578,9579,9580,9581,9582],{"id":9448,"depth":1158,"text":9449},{"id":9455,"depth":1158,"text":9456},{"id":9462,"depth":1158,"text":9463},{"id":9469,"depth":1158,"text":9470},{"id":9476,"depth":1158,"text":9477},{"id":9491,"depth":469,"text":9492},{"id":1092,"depth":469,"text":1092},"wiki:java:mysql-bplus-tree-vs-btree","从磁盘页、树高、扇出、范围查询和 InnoDB 索引组织方式出发，解释 MySQL 为何采用 B+ 树，并对比 B 树、红黑树与哈希索引。",{},76,"\u002Fjava\u002Fmysql-bplus-tree-vs-btree",{"title":8552,"description":9586},"java\u002Fmysql-bplus-tree-vs-btree","2026-07-25","go1AzXFgta1vTlcSLwEBOPoFDDAlbZSlEJktzi-QTb0",{"id":9595,"title":9596,"body":9597,"commentId":13331,"description":13332,"difficulty":2636,"draft":1208,"extension":1209,"meta":13333,"navigation":1211,"order":13334,"path":13335,"section":1214,"seo":13336,"stem":13337,"updated":9592,"__hash__":13338},"java\u002Fjava\u002Fmysql-index-failure-scenarios.md","MySQL 索引在什么情况下会失效？",{"type":7,"value":9598,"toc":13264},[9599,9602,9604,9607,9638,9641,9685,9707,9709,9714,9716,9725,9727,9731,9734,9738,9741,9744,9753,9756,9774,9777,9781,9784,9786,9795,9798,9827,9842,9851,9855,9858,9864,9867,9870,9874,9877,9885,9902,9905,9911,9914,9938,9951,9954,9974,9977,9980,9984,9987,9992,9995,9998,10006,10008,10017,10020,10044,10047,10057,10061,10067,10076,10079,10097,10107,10110,10133,10136,10156,10159,10168,10171,10174,10183,10188,10194,10197,10206,10221,10224,10233,10236,10251,10254,10262,10265,10285,10287,10301,10305,10308,10323,10326,10335,10338,10347,10350,10353,10367,10370,10373,10378,10382,10384,10393,10396,10405,10412,10415,10430,10433,10436,10447,10457,10461,10468,10490,10497,10500,10520,10525,10529,10536,10539,10554,10556,10578,10581,10587,10593,10599,10602,10666,10672,10682,10705,10708,10714,10719,10726,10740,10749,10752,10775,10778,10782,10789,10799,10801,10815,10818,10827,10830,10833,10837,10840,10860,10863,10868,10871,10877,10880,10886,10890,10893,10911,10917,10923,10926,10929,10953,10957,10963,10966,10974,10976,10998,11007,11010,11027,11032,11035,11052,11055,11057,11065,11068,11095,11104,11110,11115,11136,11147,11153,11166,11169,11194,11209,11212,11215,11245,11259,11265,11276,11289,11292,11295,11298,11307,11310,11325,11328,11331,11340,11347,11356,11359,11368,11371,11376,11383,11395,11401,11405,11408,11422,11425,11428,11437,11440,11443,11447,11450,11473,11476,11568,11575,11587,11590,11595,11598,11604,11607,11609,11635,11645,11653,11661,11664,11670,11676,11682,11687,11690,11720,11723,11741,11744,11762,11765,11771,11776,11785,11800,11803,11811,11816,11821,11826,11834,11839,11846,11848,11876,11894,11905,11911,11918,11925,11969,11975,11980,11982,11990,12006,12012,12018,12024,12029,12038,12046,12049,12054,12073,12088,12095,12100,12103,12126,12129,12135,12141,12179,12181,12190,12198,12201,12209,12212,12218,12228,12234,12242,12245,12264,12269,12275,12281,12285,12288,12291,12299,12301,12325,12333,12336,12342,12350,12356,12359,12364,12371,12377,12380,12394,12397,12404,12409,12416,12421,12427,12439,12441,12450,12465,12475,12478,12484,12489,12492,12509,12517,12525,12532,12537,12540,12546,12549,12570,12572,12590,12595,12601,12604,12623,12629,12633,12773,12778,12783,12787,12794,12814,12824,12829,12851,12854,12860,12863,12895,12898,12973,12978,12984,12987,12990,13004,13011,13014,13020,13029,13032,13038,13041,13044,13050,13055,13058,13089,13093,13096,13143,13147,13151,13154,13158,13161,13171,13174,13180,13183,13187,13190,13197,13200,13204,13262],[10,9600,9596],{"id":9601},"mysql-索引在什么情况下会失效",[14,9603,16],{"id":16},[18,9605,9606],{},"所谓“索引失效”并不是 MySQL 的正式术语，实际需要区分三种情况：",[22,9608,9609,9616,9635],{},[25,9610,9611,9612,9615],{},"SQL 写法或索引结构决定了索引无法用于快速定位，例如联合索引没有满足最左前缀、查询的是索引列的计算结果但索引只保存原始列值、",[119,9613,9614],{},"LIKE"," 以通配符开头。",[25,9617,9618,9619,9622,9623,9626,9627,9630,9631,9634],{},"索引可以使用，但只能使用其中一部分。例如联合索引 ",[119,9620,9621],{},"(a,b,c)"," 查询 ",[119,9624,9625],{},"a=1 AND b>10 AND c=3"," 时，通常可以用 ",[119,9628,9629],{},"a、b"," 确定扫描范围，",[119,9632,9633],{},"c"," 可能通过索引条件下推继续过滤，却不能进一步缩小连续的索引扫描区间。",[25,9636,9637],{},"索引具备使用条件，但优化器估算全表扫描成本更低，于是主动不选。例如查询返回大量数据、索引选择性很低或回表成本过高。",[18,9639,9640],{},"常见场景包括：",[174,9642,9643,9646,9649,9652,9661,9667,9679,9682],{},[25,9644,9645],{},"联合索引不满足最左前缀；",[25,9647,9648],{},"在索引列上执行函数、计算或不匹配的表达式；",[25,9650,9651],{},"比较两侧数据类型、字符集或排序规则不兼容，引发隐式转换；",[25,9653,9654,964,9657,9660],{},[119,9655,9656],{},"LIKE '%keyword'",[119,9658,9659],{},"LIKE '%keyword%'"," 以通配符开头；",[25,9662,9663,9666],{},[119,9664,9665],{},"OR"," 的某个分支缺少可用索引，且优化器无法采用合适的 Index Merge；",[25,9668,9669,892,9672,892,9675,9678],{},[119,9670,9671],{},"!=",[119,9673,9674],{},"NOT IN",[119,9676,9677],{},"IS NOT NULL"," 等条件命中数据过多；",[25,9680,9681],{},"查询需要返回表中很大比例的数据，优化器认为全表扫描更便宜；",[25,9683,9684],{},"索引统计信息不准确，导致优化器成本估算偏差。",[18,9686,9687,9688,9691,9692,964,9695,9698,9699,9702,9703,9706],{},"判断索引是否真正有效，不能只看 ",[119,9689,9690],{},"possible_keys","，应使用 ",[119,9693,9694],{},"EXPLAIN",[119,9696,9697],{},"EXPLAIN ANALYZE","，重点观察实际选择的 ",[119,9700,9701],{},"key","、访问类型 ",[119,9704,9705],{},"type","、预计或实际扫描行数，以及是否发生大量回表。",[196,9708,7181],{"id":7181},[57,9710,9711],{},[18,9712,9713],{},"索引失效要区分“不能用、只用一部分、能用但优化器不选”；排查时先看索引结构和 SQL 写法，再用执行计划验证实际扫描成本。",[14,9715,66],{"id":66},[18,9717,9718],{},[70,9719,9722],{"href":9720,"target":73,"rel":9721},"\u002Fimages\u002Fwiki\u002Fjava\u002Fmysql-index-failure-scenarios.svg",[75,76],[78,9723],{"src":9720,"alt":9724},"MySQL 索引失效的三类情况与排查路径",[14,9726,83],{"id":83},[14,9728,9730],{"id":9729},"一先区分三种索引失效","一、先区分三种“索引失效”",[18,9732,9733],{},"日常讨论经常把所有没有达到预期性能的情况都叫作“索引失效”，但这会掩盖真正的问题。",[196,9735,9737],{"id":9736},"_1-索引无法用于定位","1. 索引无法用于定位",[18,9739,9740],{},"索引的有序结构与查询条件不匹配，MySQL 无法根据该条件确定一个有效的索引查找范围。",[18,9742,9743],{},"例如索引为：",[204,9745,9747],{"className":7483,"code":9746,"language":7485,"meta":210,"style":210},"KEY idx_user_time (user_id, created_at)\n",[119,9748,9749],{"__ignoreMap":210},[460,9750,9751],{"class":462,"line":463},[460,9752,9746],{},[18,9754,9755],{},"查询只使用第二列：",[204,9757,9759],{"className":7483,"code":9758,"language":7485,"meta":210,"style":210},"SELECT *\nFROM orders\nWHERE created_at >= '2026-07-01';\n",[119,9760,9761,9765,9769],{"__ignoreMap":210},[460,9762,9763],{"class":462,"line":463},[460,9764,9019],{},[460,9766,9767],{"class":462,"line":469},[460,9768,9024],{},[460,9770,9771],{"class":462,"line":1158},[460,9772,9773],{},"WHERE created_at >= '2026-07-01';\n",[18,9775,9776],{},"传统的联合索引范围查找无法绕过第一列，直接按照第二列定位连续区间。",[196,9778,9780],{"id":9779},"_2-只使用了部分索引能力","2. 只使用了部分索引能力",[18,9782,9783],{},"SQL 使用了索引，但索引没有把扫描范围缩小到预期程度。",[18,9785,666],{},[204,9787,9789],{"className":7483,"code":9788,"language":7485,"meta":210,"style":210},"KEY idx_abc (a, b, c)\n",[119,9790,9791],{"__ignoreMap":210},[460,9792,9793],{"class":462,"line":463},[460,9794,9788],{},[18,9796,9797],{},"查询：",[204,9799,9801],{"className":7483,"code":9800,"language":7485,"meta":210,"style":210},"SELECT *\nFROM t\nWHERE a = 1\n  AND b > 10\n  AND c = 3;\n",[119,9802,9803,9807,9812,9817,9822],{"__ignoreMap":210},[460,9804,9805],{"class":462,"line":463},[460,9806,9019],{},[460,9808,9809],{"class":462,"line":469},[460,9810,9811],{},"FROM t\n",[460,9813,9814],{"class":462,"line":1158},[460,9815,9816],{},"WHERE a = 1\n",[460,9818,9819],{"class":462,"line":2373},[460,9820,9821],{},"  AND b > 10\n",[460,9823,9824],{"class":462,"line":2379},[460,9825,9826],{},"  AND c = 3;\n",[18,9828,9829,9830,9833,9834,9837,9838,9841],{},"通常可以使用 ",[119,9831,9832],{},"a=1 AND b>10"," 构造索引扫描范围。由于 ",[119,9835,9836],{},"b"," 是范围条件，",[119,9839,9840],{},"c=3"," 一般不能继续把这个范围切成更小的连续区间。",[18,9843,9844,9845,9847,9848,9850],{},"但这不等于 ",[119,9846,9633],{}," 完全没用。满足条件时，MySQL 可以通过索引条件下推（Index Condition Pushdown，ICP），在索引层先判断 ",[119,9849,9840],{},"，减少回表数量。",[196,9852,9854],{"id":9853},"_3-优化器主动不选择索引","3. 优化器主动不选择索引",[18,9856,9857],{},"索引本身可用，但 MySQL 优化器根据统计信息估算：",[204,9859,9862],{"className":9860,"code":9861,"language":209,"meta":210},[207],"使用二级索引扫描 + 大量回表\n                >\n顺序扫描整张表\n",[119,9863,9861],{"__ignoreMap":210},[18,9865,9866],{},"于是执行计划选择全表扫描。",[18,9868,9869],{},"这种情况不是索引结构失效，而是成本选择。强行使用索引不一定更快。",[14,9871,9873],{"id":9872},"二联合索引不满足最左前缀","二、联合索引不满足最左前缀",[18,9875,9876],{},"假设存在联合索引：",[204,9878,9879],{"className":7483,"code":9788,"language":7485,"meta":210,"style":210},[119,9880,9881],{"__ignoreMap":210},[460,9882,9883],{"class":462,"line":463},[460,9884,9788],{},[18,9886,9887,9888,9890,9891,9893,9894,9890,9896,9898,9899,9901],{},"B+ 树中的记录首先按 ",[119,9889,70],{}," 排序；",[119,9892,70],{}," 相同时，再按 ",[119,9895,9836],{},[119,9897,9629],{}," 都相同时，才按 ",[119,9900,9633],{}," 排序。",[18,9903,9904],{},"可以近似理解为：",[204,9906,9909],{"className":9907,"code":9908,"language":209,"meta":210},[207],"(a=1, b=1, c=1)\n(a=1, b=1, c=5)\n(a=1, b=2, c=2)\n(a=2, b=1, c=3)\n",[119,9910,9908],{"__ignoreMap":210},[18,9912,9913],{},"因此以下条件通常能够使用联合索引定位：",[204,9915,9917],{"className":7483,"code":9916,"language":7485,"meta":210,"style":210},"WHERE a = 1\nWHERE a = 1 AND b = 2\nWHERE a = 1 AND b = 2 AND c = 3\nWHERE a = 1 AND c = 3\n",[119,9918,9919,9923,9928,9933],{"__ignoreMap":210},[460,9920,9921],{"class":462,"line":463},[460,9922,9816],{},[460,9924,9925],{"class":462,"line":469},[460,9926,9927],{},"WHERE a = 1 AND b = 2\n",[460,9929,9930],{"class":462,"line":1158},[460,9931,9932],{},"WHERE a = 1 AND b = 2 AND c = 3\n",[460,9934,9935],{"class":462,"line":2373},[460,9936,9937],{},"WHERE a = 1 AND c = 3\n",[18,9939,9940,9941,9944,9945,9947,9948,9950],{},"最后一个条件仍然可以通过 ",[119,9942,9943],{},"a=1"," 定位范围，只是缺少 ",[119,9946,9836],{}," 后，",[119,9949,9840],{}," 通常不能继续缩小查找区间。",[18,9952,9953],{},"以下查询没有使用最左列：",[204,9955,9957],{"className":7483,"code":9956,"language":7485,"meta":210,"style":210},"WHERE b = 2\nWHERE c = 3\nWHERE b = 2 AND c = 3\n",[119,9958,9959,9964,9969],{"__ignoreMap":210},[460,9960,9961],{"class":462,"line":463},[460,9962,9963],{},"WHERE b = 2\n",[460,9965,9966],{"class":462,"line":469},[460,9967,9968],{},"WHERE c = 3\n",[460,9970,9971],{"class":462,"line":1158},[460,9972,9973],{},"WHERE b = 2 AND c = 3\n",[18,9975,9976],{},"它们通常无法使用传统的最左前缀方式直接定位。",[18,9978,9979],{},"需要注意，MySQL 8.0 在满足条件时可能使用 Skip Scan，把缺失的最左列按多个可能值分别扫描。因此“缺少最左列一定完全不用索引”也不是绝对结论，最终必须以执行计划为准。",[14,9981,9983],{"id":9982},"三范围条件之后的列还能不能使用","三、范围条件之后的列还能不能使用",[18,9985,9986],{},"常见说法是：",[57,9988,9989],{},[18,9990,9991],{},"联合索引遇到范围查询后，后面的列全部失效。",[18,9993,9994],{},"这个说法过于绝对。",[18,9996,9997],{},"索引为：",[204,9999,10000],{"className":7483,"code":9788,"language":7485,"meta":210,"style":210},[119,10001,10002],{"__ignoreMap":210},[460,10003,10004],{"class":462,"line":463},[460,10005,9788],{},[18,10007,9797],{},[204,10009,10011],{"className":7483,"code":10010,"language":7485,"meta":210,"style":210},"WHERE a = 1 AND b > 10 AND c = 3\n",[119,10012,10013],{"__ignoreMap":210},[460,10014,10015],{"class":462,"line":463},[460,10016,10010],{},[18,10018,10019],{},"更准确的理解是：",[174,10021,10022,10030,10035,10041],{},[25,10023,10024,2084,10026,10029],{},[119,10025,9943],{},[119,10027,10028],{},"b>10"," 可以共同确定扫描范围；",[25,10031,10032,10034],{},[119,10033,9840],{}," 通常不能继续缩小这个连续范围；",[25,10036,10037,10038,10040],{},"如果启用了 ICP，",[119,10039,9840],{}," 仍可能在索引层参与过滤；",[25,10042,10043],{},"如果查询列都包含在索引中，还可能形成覆盖索引，避免回表。",[18,10045,10046],{},"所以“能否参与范围定位”“能否参与索引层过滤”和“能否避免回表”是三个不同问题。",[18,10048,10049,10050,10053,10054,10056],{},"另外，",[119,10051,10052],{},"IN"," 在很多情况下会被转换成多个等值范围，不应简单地把所有 ",[119,10055,10052],{}," 都视为遇到范围后索引失效。",[14,10058,10060],{"id":10059},"四在索引列上进行函数或计算","四、在索引列上进行函数或计算",[18,10062,7706,10063,10066],{},[119,10064,10065],{},"created_at"," 上有普通索引：",[204,10068,10070],{"className":7483,"code":10069,"language":7485,"meta":210,"style":210},"KEY idx_created_at (created_at)\n",[119,10071,10072],{"__ignoreMap":210},[460,10073,10074],{"class":462,"line":463},[460,10075,10069],{},[18,10077,10078],{},"下面的写法把函数作用在索引列上：",[204,10080,10082],{"className":7483,"code":10081,"language":7485,"meta":210,"style":210},"SELECT *\nFROM orders\nWHERE DATE(created_at) = '2026-07-25';\n",[119,10083,10084,10088,10092],{"__ignoreMap":210},[460,10085,10086],{"class":462,"line":463},[460,10087,9019],{},[460,10089,10090],{"class":462,"line":469},[460,10091,9024],{},[460,10093,10094],{"class":462,"line":1158},[460,10095,10096],{},"WHERE DATE(created_at) = '2026-07-25';\n",[18,10098,10099,10100,10102,10103,10106],{},"普通索引保存的是原始 ",[119,10101,10065],{}," 值，而不是 ",[119,10104,10105],{},"DATE(created_at)"," 的计算结果。MySQL 通常无法直接根据普通索引定位函数结果。",[18,10108,10109],{},"可以改写为范围条件：",[204,10111,10113],{"className":7483,"code":10112,"language":7485,"meta":210,"style":210},"SELECT *\nFROM orders\nWHERE created_at >= '2026-07-25 00:00:00'\n  AND created_at \u003C  '2026-07-26 00:00:00';\n",[119,10114,10115,10119,10123,10128],{"__ignoreMap":210},[460,10116,10117],{"class":462,"line":463},[460,10118,9019],{},[460,10120,10121],{"class":462,"line":469},[460,10122,9024],{},[460,10124,10125],{"class":462,"line":1158},[460,10126,10127],{},"WHERE created_at >= '2026-07-25 00:00:00'\n",[460,10129,10130],{"class":462,"line":2373},[460,10131,10132],{},"  AND created_at \u003C  '2026-07-26 00:00:00';\n",[18,10134,10135],{},"类似问题还包括：",[204,10137,10139],{"className":7483,"code":10138,"language":7485,"meta":210,"style":210},"WHERE amount + 1 = 100\nWHERE LOWER(username) = 'tom'\nWHERE CAST(order_no AS UNSIGNED) = 1001\n",[119,10140,10141,10146,10151],{"__ignoreMap":210},[460,10142,10143],{"class":462,"line":463},[460,10144,10145],{},"WHERE amount + 1 = 100\n",[460,10147,10148],{"class":462,"line":469},[460,10149,10150],{},"WHERE LOWER(username) = 'tom'\n",[460,10152,10153],{"class":462,"line":1158},[460,10154,10155],{},"WHERE CAST(order_no AS UNSIGNED) = 1001\n",[18,10157,10158],{},"应优先把运算移动到常量一侧：",[204,10160,10162],{"className":7483,"code":10161,"language":7485,"meta":210,"style":210},"WHERE amount = 99\n",[119,10163,10164],{"__ignoreMap":210},[460,10165,10166],{"class":462,"line":463},[460,10167,10161],{},[18,10169,10170],{},"这里所说的“对索引列进行未建立函数索引的运算”，可以用下面的例子理解。",[18,10172,10173],{},"普通索引：",[204,10175,10177],{"className":7483,"code":10176,"language":7485,"meta":210,"style":210},"KEY idx_amount (amount)\n",[119,10178,10179],{"__ignoreMap":210},[460,10180,10181],{"class":462,"line":463},[460,10182,10176],{},[18,10184,10185,10186,3213],{},"保存并排序的是原始 ",[119,10187,7709],{},[204,10189,10192],{"className":10190,"code":10191,"language":209,"meta":210},[207],"10 → 20 → 30 → 40\n",[119,10193,10191],{"__ignoreMap":210},[18,10195,10196],{},"查询条件却是：",[204,10198,10200],{"className":7483,"code":10199,"language":7485,"meta":210,"style":210},"WHERE amount + 1 = 31\n",[119,10201,10202],{"__ignoreMap":210},[460,10203,10204],{"class":462,"line":463},[460,10205,10199],{},[18,10207,10208,10209,10212,10213,10216,10217,10220],{},"MySQL 需要比较的是 ",[119,10210,10211],{},"amount + 1"," 的结果，而普通索引中没有保存这个结果。它通常无法直接在 ",[119,10214,10215],{},"idx_amount"," 中查找 ",[119,10218,10219],{},"31","，而需要对候选记录计算表达式。",[18,10222,10223],{},"这个条件可以直接改写为：",[204,10225,10227],{"className":7483,"code":10226,"language":7485,"meta":210,"style":210},"WHERE amount = 30\n",[119,10228,10229],{"__ignoreMap":210},[460,10230,10231],{"class":462,"line":463},[460,10232,10226],{},[18,10234,10235],{},"如果业务确实需要长期按照某个表达式查询，MySQL 8.0.13 及以上版本支持函数索引：",[204,10237,10239],{"className":7483,"code":10238,"language":7485,"meta":210,"style":210},"CREATE INDEX idx_amount_plus_one\nON orders ((amount + 1));\n",[119,10240,10241,10246],{"__ignoreMap":210},[460,10242,10243],{"class":462,"line":463},[460,10244,10245],{},"CREATE INDEX idx_amount_plus_one\n",[460,10247,10248],{"class":462,"line":469},[460,10249,10250],{},"ON orders ((amount + 1));\n",[18,10252,10253],{},"之后与索引表达式匹配的查询才可以直接检索表达式结果：",[204,10255,10256],{"className":7483,"code":10199,"language":7485,"meta":210,"style":210},[119,10257,10258],{"__ignoreMap":210},[460,10259,10260],{"class":462,"line":463},[460,10261,10199],{},[18,10263,10264],{},"也可以创建生成列，再为生成列建立索引：",[204,10266,10268],{"className":7483,"code":10267,"language":7485,"meta":210,"style":210},"ALTER TABLE orders\nADD COLUMN order_date DATE AS (DATE(created_at)),\nADD INDEX idx_order_date (order_date);\n",[119,10269,10270,10275,10280],{"__ignoreMap":210},[460,10271,10272],{"class":462,"line":463},[460,10273,10274],{},"ALTER TABLE orders\n",[460,10276,10277],{"class":462,"line":469},[460,10278,10279],{},"ADD COLUMN order_date DATE AS (DATE(created_at)),\n",[460,10281,10282],{"class":462,"line":1158},[460,10283,10284],{},"ADD INDEX idx_order_date (order_date);\n",[18,10286,3785],{},[174,10288,10289,10292,10295,10298],{},[25,10290,10291],{},"函数索引保存的是表达式计算结果，不是让任意函数都自动使用原列索引；",[25,10293,10294],{},"查询中的表达式需要与索引定义匹配；",[25,10296,10297],{},"函数索引会占用空间，也会增加写入和更新成本；",[25,10299,10300],{},"能通过等价范围条件改写的 SQL，通常优先改写，未必需要新增索引。",[14,10302,10304],{"id":10303},"五隐式类型转换","五、隐式类型转换",[18,10306,10307],{},"假设订单号是字符串：",[204,10309,10311],{"className":7483,"code":10310,"language":7485,"meta":210,"style":210},"order_no VARCHAR(32),\nKEY idx_order_no (order_no)\n",[119,10312,10313,10318],{"__ignoreMap":210},[460,10314,10315],{"class":462,"line":463},[460,10316,10317],{},"order_no VARCHAR(32),\n",[460,10319,10320],{"class":462,"line":469},[460,10321,10322],{},"KEY idx_order_no (order_no)\n",[18,10324,10325],{},"推荐使用相同类型进行比较：",[204,10327,10329],{"className":7483,"code":10328,"language":7485,"meta":210,"style":210},"WHERE order_no = '10001'\n",[119,10330,10331],{"__ignoreMap":210},[460,10332,10333],{"class":462,"line":463},[460,10334,10328],{},[18,10336,10337],{},"如果写成：",[204,10339,10341],{"className":7483,"code":10340,"language":7485,"meta":210,"style":210},"WHERE order_no = 10001\n",[119,10342,10343],{"__ignoreMap":210},[460,10344,10345],{"class":462,"line":463},[460,10346,10340],{},[18,10348,10349],{},"MySQL 需要按照类型转换规则比较字符串与数字，可能对索引列执行转换，导致无法按原有字符串顺序高效定位。",[18,10351,10352],{},"排查时要检查：",[174,10354,10355,10358,10361,10364],{},[25,10356,10357],{},"数字列是否与字符串参数比较；",[25,10359,10360],{},"字符串列的字符集是否一致；",[25,10362,10363],{},"字符串列的排序规则是否兼容；",[25,10365,10366],{},"JOIN 两侧字段的类型、长度、字符集和排序规则是否匹配。",[18,10368,10369],{},"例如两个字符串列字符集不兼容时，即使两侧都有索引，也可能无法高效使用索引完成连接。",[18,10371,10372],{},"最稳妥的原则是：",[57,10374,10375],{},[18,10376,10377],{},"SQL 参数类型应与索引列类型一致，JOIN 字段的类型和字符规则也应保持一致。",[14,10379,10381],{"id":10380},"六like-以通配符开头","六、LIKE 以通配符开头",[18,10383,9997],{},[204,10385,10387],{"className":7483,"code":10386,"language":7485,"meta":210,"style":210},"KEY idx_name (name)\n",[119,10388,10389],{"__ignoreMap":210},[460,10390,10391],{"class":462,"line":463},[460,10392,10386],{},[18,10394,10395],{},"以下前缀匹配通常可以利用 B+ 树的有序性：",[204,10397,10399],{"className":7483,"code":10398,"language":7485,"meta":210,"style":210},"WHERE name LIKE 'zhizhi%'\n",[119,10400,10401],{"__ignoreMap":210},[460,10402,10403],{"class":462,"line":463},[460,10404,10398],{},[18,10406,10407,10408,10411],{},"MySQL 可以定位到以 ",[119,10409,10410],{},"zhizhi"," 开头的索引范围。",[18,10413,10414],{},"以下写法无法确定字符串起始位置：",[204,10416,10418],{"className":7483,"code":10417,"language":7485,"meta":210,"style":210},"WHERE name LIKE '%zhizhi'\nWHERE name LIKE '%zhizhi%'\n",[119,10419,10420,10425],{"__ignoreMap":210},[460,10421,10422],{"class":462,"line":463},[460,10423,10424],{},"WHERE name LIKE '%zhizhi'\n",[460,10426,10427],{"class":462,"line":469},[460,10428,10429],{},"WHERE name LIKE '%zhizhi%'\n",[18,10431,10432],{},"普通 B+ 树索引通常无法据此快速定位范围。",[18,10434,10435],{},"如果查询是高频的任意位置文本搜索，应根据业务考虑：",[174,10437,10438,10441,10444],{},[25,10439,10440],{},"FULLTEXT 全文索引；",[25,10442,10443],{},"Elasticsearch 等搜索系统；",[25,10445,10446],{},"调整业务模型，保存可索引的前缀或结构化字段。",[18,10448,10449,10450,10452,10453,10456],{},"需要注意，如果查询需要的列全部包含在索引中，优化器仍可能选择扫描整个索引，因为索引比数据行更窄。此时执行计划中的 ",[119,10451,9705],{}," 可能是 ",[119,10454,10455],{},"index","，但这仍然是全索引扫描，并不是高效的范围查找。",[14,10458,10460],{"id":10459},"七or-中存在无法使用索引的分支","七、OR 中存在无法使用索引的分支",[18,10462,10463,10464,10467],{},"假设只有 ",[119,10465,10466],{},"user_id"," 有索引：",[204,10469,10471],{"className":7483,"code":10470,"language":7485,"meta":210,"style":210},"SELECT *\nFROM orders\nWHERE user_id = 100\n   OR remark = 'manual';\n",[119,10472,10473,10477,10481,10485],{"__ignoreMap":210},[460,10474,10475],{"class":462,"line":463},[460,10476,9019],{},[460,10478,10479],{"class":462,"line":469},[460,10480,9024],{},[460,10482,10483],{"class":462,"line":1158},[460,10484,8074],{},[460,10486,10487],{"class":462,"line":2373},[460,10488,10489],{},"   OR remark = 'manual';\n",[18,10491,10492,10493,10496],{},"即使第一部分能够使用索引，为了找出满足 ",[119,10494,10495],{},"remark='manual'"," 的记录，MySQL 仍可能需要扫描大量数据，最终选择全表扫描。",[18,10498,10499],{},"常见优化方式包括：",[22,10501,10502,10505,10511,10517],{},[25,10503,10504],{},"为确实需要的条件建立合适索引；",[25,10506,10507,10508,1426],{},"在语义允许且执行计划更优时拆成两个查询并使用 ",[119,10509,10510],{},"UNION ALL",[25,10512,10513,10514,10516],{},"检查重复数据处理，不能为了用索引直接把 ",[119,10515,9665],{}," 改坏；",[25,10518,10519],{},"观察优化器是否采用 Index Merge，而不是假设多个单列索引一定会被组合。",[18,10521,3234,10522,10524],{},[119,10523,9665],{}," 两侧分别有可用索引，MySQL 可能选择 Index Merge，也可能根据成本选择其他计划。",[196,10526,10528],{"id":10527},"index-merge-是什么","Index Merge 是什么",[18,10530,10531,10532,10535],{},"Index Merge 是 MySQL 在",[28,10533,10534],{},"同一张表","上分别扫描多个索引，再合并这些索引扫描结果的访问方式。",[18,10537,10538],{},"假设存在两个单列索引：",[204,10540,10542],{"className":7483,"code":10541,"language":7485,"meta":210,"style":210},"KEY idx_user_id (user_id),\nKEY idx_status (status)\n",[119,10543,10544,10549],{"__ignoreMap":210},[460,10545,10546],{"class":462,"line":463},[460,10547,10548],{},"KEY idx_user_id (user_id),\n",[460,10550,10551],{"class":462,"line":469},[460,10552,10553],{},"KEY idx_status (status)\n",[18,10555,9797],{},[204,10557,10559],{"className":7483,"code":10558,"language":7485,"meta":210,"style":210},"SELECT *\nFROM orders\nWHERE user_id = 100\n   OR status = 1;\n",[119,10560,10561,10565,10569,10573],{"__ignoreMap":210},[460,10562,10563],{"class":462,"line":463},[460,10564,9019],{},[460,10566,10567],{"class":462,"line":469},[460,10568,9024],{},[460,10570,10571],{"class":462,"line":1158},[460,10572,8074],{},[460,10574,10575],{"class":462,"line":2373},[460,10576,10577],{},"   OR status = 1;\n",[18,10579,10580],{},"MySQL 可以分别得到：",[204,10582,10585],{"className":10583,"code":10584,"language":209,"meta":210},[207],"idx_user_id 扫描结果：满足 user_id=100 的行 ID\nidx_status  扫描结果：满足 status=1 的行 ID\n                         ↓\n                    合并并去重\n                         ↓\n                    读取完整记录\n",[119,10586,10584],{"__ignoreMap":210},[18,10588,10589,10590,10592],{},"在 ",[119,10591,9694],{}," 中通常表现为：",[204,10594,10597],{"className":10595,"code":10596,"language":209,"meta":210},[207],"type: index_merge\nkey: idx_user_id,idx_status\nExtra: Using union(...)\n",[119,10598,10596],{"__ignoreMap":210},[18,10600,10601],{},"Index Merge 主要有三种合并方式：",[92,10603,10604,10617],{},[95,10605,10606],{},[98,10607,10608,10611,10614],{},[101,10609,10610],{},"方式",[101,10612,10613],{},"适用逻辑",[101,10615,10616],{},"处理方式",[111,10618,10619,10636,10650],{},[98,10620,10621,10626,10633],{},[116,10622,10623],{},[119,10624,10625],{},"Using intersect(...)",[116,10627,10628,10629,10632],{},"多个条件用 ",[119,10630,10631],{},"AND"," 连接",[116,10634,10635],{},"取多个索引结果的交集",[98,10637,10638,10643,10647],{},[116,10639,10640],{},[119,10641,10642],{},"Using union(...)",[116,10644,10628,10645,10632],{},[119,10646,9665],{},[116,10648,10649],{},"取多个索引结果的并集",[98,10651,10652,10657,10663],{},[116,10653,10654],{},[119,10655,10656],{},"Using sort_union(...)",[116,10658,10659,10660,10662],{},"多个范围条件用 ",[119,10661,9665],{}," 连接，但不能直接使用 union 算法",[116,10664,10665],{},"收集全部行 ID，排序、去重后再读取记录",[196,10667,10669,10671],{"id":10668},"using-sort_union-是什么意思",[119,10670,10656],{}," 是什么意思",[18,10673,10674,10675,2084,10678,10681],{},"先看一个典型查询。假设 ",[119,10676,10677],{},"key_col1",[119,10679,10680],{},"key_col2"," 分别建立了单列索引：",[204,10683,10685],{"className":7483,"code":10684,"language":7485,"meta":210,"style":210},"SELECT *\nFROM orders\nWHERE key_col1 \u003C 10\n   OR key_col2 \u003C 20;\n",[119,10686,10687,10691,10695,10700],{"__ignoreMap":210},[460,10688,10689],{"class":462,"line":463},[460,10690,9019],{},[460,10692,10693],{"class":462,"line":469},[460,10694,9024],{},[460,10696,10697],{"class":462,"line":1158},[460,10698,10699],{},"WHERE key_col1 \u003C 10\n",[460,10701,10702],{"class":462,"line":2373},[460,10703,10704],{},"   OR key_col2 \u003C 20;\n",[18,10706,10707],{},"优化器可以分别扫描两个索引，但这两个范围扫描产生的行 ID 不能直接按 union 算法边扫描边归并。此时可能采用 sort-union：",[204,10709,10712],{"className":10710,"code":10711,"language":209,"meta":210},[207],"扫描 key_col1 \u003C 10，收集行 ID\n                  \\\n                   → 汇总全部行 ID → 按行 ID 排序 → 去重 → 读取完整记录\n                  \u002F\n扫描 key_col2 \u003C 20，收集行 ID\n",[119,10713,10711],{"__ignoreMap":210},[18,10715,10716,10717,326],{},"以 InnoDB 二级索引为例，这里的“行 ID”通常可以理解为索引记录中携带的主键值。排序的对象是这些用于定位记录的标识，不是对最终查询结果执行 ",[119,10718,9090],{},[18,10720,10721,5280,10723,10725],{},[119,10722,10642],{},[119,10724,10656],{}," 的关键区别是：",[174,10727,10728,10734],{},[25,10729,10730,10733],{},[119,10731,10732],{},"union"," 可以对多个索引扫描产生的有序行 ID 流直接归并；",[25,10735,10736,10739],{},[119,10737,10738],{},"sort_union"," 需要先收集所有行 ID，再排序和去重，因此在完成收集与排序前不能开始返回记录。",[18,10741,10742,10743,10745,10746,10748],{},"所以 ",[119,10744,10738],{}," 通常比直接的 ",[119,10747,10732],{}," 多出行 ID 缓冲、排序和去重成本。候选记录很多时，优化器也可能认为全表扫描更便宜，最终不选择 Index Merge。",[18,10750,10751],{},"需要特别区分：",[174,10753,10754,10759,10766],{},[25,10755,10756,10758],{},[119,10757,10656],{}," 是同一张表内部多个索引扫描结果的合并算法；",[25,10760,10761,10762,10765],{},"SQL 的 ",[119,10763,10764],{},"UNION"," 是多个查询结果集的集合运算；",[25,10767,10768,10771,10772,10774],{},[119,10769,10770],{},"Using filesort"," 通常表示为了满足 ",[119,10773,9090],{}," 等要求进行额外排序。",[18,10776,10777],{},"这三者不是一回事。",[196,10779,10781],{"id":10780},"index-merge-从哪个版本开始支持","Index Merge 从哪个版本开始支持",[18,10783,10784,10785,10788],{},"Index Merge 从 ",[28,10786,10787],{},"MySQL 5.0"," 开始引入。在此之前，一张表在一次查询访问中通常至多选择一个索引；MySQL 5.0 开始可以在同一张表上扫描多个索引并合并结果。",[18,10790,10791,10792,892,10795,10798],{},"需要注意，Index Merge 本身是 MySQL 5.0 就有的访问方式，而用于显式影响它的 ",[119,10793,10794],{},"INDEX_MERGE",[119,10796,10797],{},"NO_INDEX_MERGE"," 优化器 Hint 是后续版本才增加的能力，不应把两者的版本混为一谈。",[18,10800,666],{},[204,10802,10804],{"className":7483,"code":10803,"language":7485,"meta":210,"style":210},"WHERE user_id = 100\n  AND status = 1\n",[119,10805,10806,10810],{"__ignoreMap":210},[460,10807,10808],{"class":462,"line":463},[460,10809,8074],{},[460,10811,10812],{"class":462,"line":469},[460,10813,10814],{},"  AND status = 1\n",[18,10816,10817],{},"优化器可能分别扫描两个索引后取交集。不过，很多情况下建立符合查询模式的联合索引：",[204,10819,10821],{"className":7483,"code":10820,"language":7485,"meta":210,"style":210},"KEY idx_user_status (user_id, status)\n",[119,10822,10823],{"__ignoreMap":210},[460,10824,10825],{"class":462,"line":463},[460,10826,10820],{},[18,10828,10829],{},"会比扫描并合并两个单列索引更直接。",[18,10831,10832],{},"因此 Index Merge 是优化器可选择的一种方案，不代表“多个单列索引一定等价于联合索引”。它也只合并同一张表上的索引扫描，不负责合并不同表的索引。",[14,10834,10836],{"id":10835},"八否定条件和低选择性条件","八、否定条件和低选择性条件",[18,10838,10839],{},"以下条件经常返回大量记录：",[204,10841,10843],{"className":7483,"code":10842,"language":7485,"meta":210,"style":210},"WHERE status != 1\nWHERE type NOT IN (1, 2)\nWHERE deleted_at IS NOT NULL\n",[119,10844,10845,10850,10855],{"__ignoreMap":210},[460,10846,10847],{"class":462,"line":463},[460,10848,10849],{},"WHERE status != 1\n",[460,10851,10852],{"class":462,"line":469},[460,10853,10854],{},"WHERE type NOT IN (1, 2)\n",[460,10856,10857],{"class":462,"line":1158},[460,10858,10859],{},"WHERE deleted_at IS NOT NULL\n",[18,10861,10862],{},"这些操作符并不意味着语法上绝对不能使用索引。真正的问题通常是选择性：",[57,10864,10865],{},[18,10866,10867],{},"如果条件会命中表中大部分记录，使用二级索引定位后再逐条回表，可能比全表扫描更贵。",[18,10869,10870],{},"同理，以下字段即使有索引，也可能因为区分度太低而不被选择：",[204,10872,10875],{"className":10873,"code":10874,"language":209,"meta":210},[207],"性别\n是否删除\n只有少量枚举值的状态\n大量相同值的业务类型\n",[119,10876,10874],{"__ignoreMap":210},[18,10878,10879],{},"低基数字段并非永远不适合索引。如果它与其他字段组成联合索引、查询命中比例很低，或者能够形成覆盖索引，依然可能产生价值。",[18,10881,10049,10882,10885],{},[119,10883,10884],{},"IS NULL"," 可以使用索引，不应把它列为固定的索引失效规则。",[14,10887,10889],{"id":10888},"九查询返回数据过多","九、查询返回数据过多",[18,10891,10892],{},"假设表中有 100 万行，而条件预计返回 80 万行：",[204,10894,10896],{"className":7483,"code":10895,"language":7485,"meta":210,"style":210},"SELECT *\nFROM orders\nWHERE status = 1;\n",[119,10897,10898,10902,10906],{"__ignoreMap":210},[460,10899,10900],{"class":462,"line":463},[460,10901,9019],{},[460,10903,10904],{"class":462,"line":469},[460,10905,9024],{},[460,10907,10908],{"class":462,"line":1158},[460,10909,10910],{},"WHERE status = 1;\n",[18,10912,3234,10913,10916],{},[119,10914,10915],{},"status"," 是二级索引，执行过程可能是：",[204,10918,10921],{"className":10919,"code":10920,"language":209,"meta":210},[207],"扫描大量二级索引记录\n        ↓\n根据主键逐条回表\n        ↓\n读取完整行\n",[119,10922,10920],{"__ignoreMap":210},[18,10924,10925],{},"大量离散回表的成本可能高于直接扫描聚簇索引，因此优化器会主动选择全表扫描。",[18,10927,10928],{},"可以从以下方向优化：",[174,10930,10931,10934,10937,10940,10943,10946],{},[25,10932,10933],{},"确认业务是否真的需要返回如此多的数据；",[25,10935,10936],{},"增加更有区分度的查询条件；",[25,10938,10939],{},"建立符合查询模式的联合索引；",[25,10941,10942],{},"只查询必要字段，评估覆盖索引；",[25,10944,10945],{},"分页或分批处理；",[25,10947,10948,10949,10952],{},"避免用 ",[119,10950,10951],{},"FORCE INDEX"," 掩盖不合理的数据访问方式。",[14,10954,10956],{"id":10955},"十order-by-或-group-by-没有匹配索引顺序","十、ORDER BY 或 GROUP BY 没有匹配索引顺序",[18,10958,10959,10960,10962],{},"索引不仅可以用于 ",[119,10961,7962],{}," 过滤，也可以帮助排序和分组，但需要满足相应的顺序条件。",[18,10964,10965],{},"例如索引：",[204,10967,10968],{"className":7483,"code":9746,"language":7485,"meta":210,"style":210},[119,10969,10970],{"__ignoreMap":210},[460,10971,10972],{"class":462,"line":463},[460,10973,9746],{},[18,10975,9797],{},[204,10977,10979],{"className":7483,"code":10978,"language":7485,"meta":210,"style":210},"SELECT *\nFROM orders\nWHERE user_id = 100\nORDER BY created_at;\n",[119,10980,10981,10985,10989,10993],{"__ignoreMap":210},[460,10982,10983],{"class":462,"line":463},[460,10984,9019],{},[460,10986,10987],{"class":462,"line":469},[460,10988,9024],{},[460,10990,10991],{"class":462,"line":1158},[460,10992,8074],{},[460,10994,10995],{"class":462,"line":2373},[460,10996,10997],{},"ORDER BY created_at;\n",[18,10999,11000,11001,11003,11004,11006],{},"通常可以利用索引中同一 ",[119,11002,10466],{}," 下 ",[119,11005,10065],{}," 有序的特点。",[18,11008,11009],{},"但下面的查询未限定最左列：",[204,11011,11013],{"className":7483,"code":11012,"language":7485,"meta":210,"style":210},"SELECT *\nFROM orders\nORDER BY created_at;\n",[119,11014,11015,11019,11023],{"__ignoreMap":210},[460,11016,11017],{"class":462,"line":463},[460,11018,9019],{},[460,11020,11021],{"class":462,"line":469},[460,11022,9024],{},[460,11024,11025],{"class":462,"line":1158},[460,11026,10997],{},[18,11028,11029,11031],{},[119,11030,10065],{}," 在整棵联合索引中并不是全局连续有序，通常无法直接利用该索引消除排序。",[18,11033,11034],{},"以下情况也可能无法直接使用索引顺序：",[174,11036,11037,11040,11043,11046,11049],{},[25,11038,11039],{},"排序列顺序与联合索引不匹配；",[25,11041,11042],{},"联合索引中，排序列前面存在没有被固定为常量的索引列；",[25,11044,11045],{},"对排序列执行表达式或函数；",[25,11047,11048],{},"多张表连接后按非驱动表字段排序；",[25,11050,11051],{},"混合升降序与索引定义不匹配。",[196,11053,11054],{"id":11054},"过滤条件如何影响索引顺序",[18,11056,9876],{},[204,11058,11059],{"className":7483,"code":9788,"language":7485,"meta":210,"style":210},[119,11060,11061],{"__ignoreMap":210},[460,11062,11063],{"class":462,"line":463},[460,11064,9788],{},[18,11066,11067],{},"下面的查询可以利用索引顺序：",[204,11069,11071],{"className":7483,"code":11070,"language":7485,"meta":210,"style":210},"SELECT *\nFROM t\nWHERE a = 1\n  AND b = 2\nORDER BY c;\n",[119,11072,11073,11077,11081,11085,11090],{"__ignoreMap":210},[460,11074,11075],{"class":462,"line":463},[460,11076,9019],{},[460,11078,11079],{"class":462,"line":469},[460,11080,9811],{},[460,11082,11083],{"class":462,"line":1158},[460,11084,9816],{},[460,11086,11087],{"class":462,"line":2373},[460,11088,11089],{},"  AND b = 2\n",[460,11091,11092],{"class":462,"line":2379},[460,11093,11094],{},"ORDER BY c;\n",[18,11096,11097,11098,11100,11101,11103],{},"因为 ",[119,11099,9629],{}," 都固定为常量，剩余记录天然按照 ",[119,11102,9633],{}," 排序：",[204,11105,11108],{"className":11106,"code":11107,"language":209,"meta":210},[207],"(a=1, b=2, c=1)\n(a=1, b=2, c=2)\n(a=1, b=2, c=3)\n",[119,11109,11107],{"__ignoreMap":210},[18,11111,11112,11113,3213],{},"如果只固定 ",[119,11114,70],{},[204,11116,11118],{"className":7483,"code":11117,"language":7485,"meta":210,"style":210},"SELECT *\nFROM t\nWHERE a = 1\nORDER BY c;\n",[119,11119,11120,11124,11128,11132],{"__ignoreMap":210},[460,11121,11122],{"class":462,"line":463},[460,11123,9019],{},[460,11125,11126],{"class":462,"line":469},[460,11127,9811],{},[460,11129,11130],{"class":462,"line":1158},[460,11131,9816],{},[460,11133,11134],{"class":462,"line":2373},[460,11135,11094],{},[18,11137,11138,11139,11141,11142,11144,11145,11103],{},"索引记录先按照 ",[119,11140,9836],{}," 排序，再在相同 ",[119,11143,9836],{}," 中按照 ",[119,11146,9633],{},[204,11148,11151],{"className":11149,"code":11150,"language":209,"meta":210},[207],"(a=1, b=1, c=1)\n(a=1, b=1, c=9)\n(a=1, b=2, c=2)\n(a=1, b=2, c=8)\n",[119,11152,11150],{"__ignoreMap":210},[18,11154,11155,11156,11158,11159,11162,11163,326],{},"从整体看，",[119,11157,9633],{}," 的顺序是 ",[119,11160,11161],{},"1、9、2、8","，并不是全局有序，因此通常仍需 ",[119,11164,11165],{},"filesort",[18,11167,11168],{},"范围条件也可能产生类似效果：",[204,11170,11172],{"className":7483,"code":11171,"language":7485,"meta":210,"style":210},"SELECT *\nFROM t\nWHERE a = 1\n  AND b > 10\nORDER BY c;\n",[119,11173,11174,11178,11182,11186,11190],{"__ignoreMap":210},[460,11175,11176],{"class":462,"line":463},[460,11177,9019],{},[460,11179,11180],{"class":462,"line":469},[460,11181,9811],{},[460,11183,11184],{"class":462,"line":1158},[460,11185,9816],{},[460,11187,11188],{"class":462,"line":2373},[460,11189,9821],{},[460,11191,11192],{"class":462,"line":2379},[460,11193,11094],{},[18,11195,11196,11197,11199,11200,11202,11203,11205,11206,11208],{},"扫描结果首先按不同的 ",[119,11198,9836],{}," 值分组，每个 ",[119,11201,9836],{}," 内部才按 ",[119,11204,9633],{}," 排序，所以不能直接保证全局 ",[119,11207,9633],{}," 有序。",[196,11210,11211],{"id":11211},"为什么按非驱动表字段排序通常需要额外排序",[18,11213,11214],{},"假设查询：",[204,11216,11218],{"className":7483,"code":11217,"language":7485,"meta":210,"style":210},"SELECT o.id, u.name\nFROM orders o\nJOIN users u ON u.id = o.user_id\nWHERE o.created_at >= '2026-07-01'\nORDER BY u.name;\n",[119,11219,11220,11225,11230,11235,11240],{"__ignoreMap":210},[460,11221,11222],{"class":462,"line":463},[460,11223,11224],{},"SELECT o.id, u.name\n",[460,11226,11227],{"class":462,"line":469},[460,11228,11229],{},"FROM orders o\n",[460,11231,11232],{"class":462,"line":1158},[460,11233,11234],{},"JOIN users u ON u.id = o.user_id\n",[460,11236,11237],{"class":462,"line":2373},[460,11238,11239],{},"WHERE o.created_at >= '2026-07-01'\n",[460,11241,11242],{"class":462,"line":2379},[460,11243,11244],{},"ORDER BY u.name;\n",[18,11246,11247,11248,11251,11252,11255,11256,11258],{},"假设优化器选择 ",[119,11249,11250],{},"orders"," 作为第一个非 ",[119,11253,11254],{},"const"," 表，也就是先按订单时间读取订单，再逐条根据 ",[119,11257,10466],{}," 查询用户：",[204,11260,11263],{"className":11261,"code":11262,"language":209,"meta":210},[207],"按 orders.created_at 读取订单\n            ↓\n逐条查找对应 users\n            ↓\n得到的 u.name 没有全局顺序\n            ↓\n对连接结果执行 filesort\n",[119,11264,11262],{"__ignoreMap":210},[18,11266,11267,11268,11271,11272,11275],{},"即使 ",[119,11269,11270],{},"users.name"," 上有索引，这个索引也只保证从 ",[119,11273,11274],{},"users"," 出发扫描时的姓名顺序，不能保证“先读取订单、再查用户”得到的连接结果按姓名有序。",[18,11277,11278,11279,11281,11282,11284,11285,11288],{},"如果优化器能够把 ",[119,11280,11274],{}," 调整为驱动表，并且从 ",[119,11283,11270],{}," 索引按顺序读取，再连接订单，就可能利用该顺序。但是否能够这样执行，还取决于过滤条件、连接关系、成本估算和 ",[119,11286,11287],{},"LIMIT"," 等因素。",[196,11290,11291],{"id":11291},"索引方向与查询方向如何匹配",[18,11293,11294],{},"MySQL 8.0 支持降序索引，也支持反向扫描 B+ 树索引。",[18,11296,11297],{},"索引：",[204,11299,11301],{"className":7483,"code":11300,"language":7485,"meta":210,"style":210},"KEY idx_ab (a ASC, b ASC)\n",[119,11302,11303],{"__ignoreMap":210},[460,11304,11305],{"class":462,"line":463},[460,11306,11300],{},[18,11308,11309],{},"通常可以支持：",[204,11311,11313],{"className":7483,"code":11312,"language":7485,"meta":210,"style":210},"ORDER BY a ASC,  b ASC\nORDER BY a DESC, b DESC\n",[119,11314,11315,11320],{"__ignoreMap":210},[460,11316,11317],{"class":462,"line":463},[460,11318,11319],{},"ORDER BY a ASC,  b ASC\n",[460,11321,11322],{"class":462,"line":469},[460,11323,11324],{},"ORDER BY a DESC, b DESC\n",[18,11326,11327],{},"第二条可以通过整体反向扫描完成。",[18,11329,11330],{},"但混合方向：",[204,11332,11334],{"className":7483,"code":11333,"language":7485,"meta":210,"style":210},"ORDER BY a ASC, b DESC\n",[119,11335,11336],{"__ignoreMap":210},[460,11337,11338],{"class":462,"line":463},[460,11339,11333],{},[18,11341,11342,11343,11346],{},"与 ",[119,11344,11345],{},"(a ASC, b ASC)"," 的排序关系不同。要直接使用索引顺序，应建立方向相匹配的索引：",[204,11348,11350],{"className":7483,"code":11349,"language":7485,"meta":210,"style":210},"KEY idx_ab_mixed (a ASC, b DESC)\n",[119,11351,11352],{"__ignoreMap":210},[460,11353,11354],{"class":462,"line":463},[460,11355,11349],{},[18,11357,11358],{},"这个索引也可以通过整体反向扫描支持：",[204,11360,11362],{"className":7483,"code":11361,"language":7485,"meta":210,"style":210},"ORDER BY a DESC, b ASC\n",[119,11363,11364],{"__ignoreMap":210},[460,11365,11366],{"class":462,"line":463},[460,11367,11361],{},[18,11369,11370],{},"所以更准确的规则是：",[57,11372,11373],{},[18,11374,11375],{},"所有列同向时，升序索引可以正向或反向扫描；升降序混合时，索引各列的方向关系需要与查询一致，或者与查询整体相反。",[18,11377,11378,11379,11382],{},"MySQL 5.7 及更早版本虽然允许在索引定义中书写 ",[119,11380,11381],{},"DESC","，但通常不会真正按降序存储键值。讨论索引方向时必须结合实际 MySQL 版本。",[18,11384,11385,11386,11388,11389,11392,11393,326],{},"此时 ",[119,11387,9694],{}," 的 ",[119,11390,11391],{},"Extra"," 中可能出现 ",[119,11394,10770],{},[18,11396,11397,11398,11400],{},"需要注意，",[119,11399,10770],{}," 表示没有直接利用索引顺序完成排序，并不保证真的使用磁盘文件；数据较小时也可能在内存中排序。",[14,11402,11404],{"id":11403},"十一索引统计信息不准确","十一、索引统计信息不准确",[18,11406,11407],{},"MySQL 优化器依赖统计信息估算：",[174,11409,11410,11413,11416,11419],{},[25,11411,11412],{},"条件会命中多少行；",[25,11414,11415],{},"不同索引的选择性；",[25,11417,11418],{},"扫描和回表成本；",[25,11420,11421],{},"JOIN 的顺序及成本。",[18,11423,11424],{},"如果数据分布发生剧烈变化，而统计信息未能反映真实情况，优化器可能选错索引或改用全表扫描。",[18,11426,11427],{},"可以先通过执行计划验证估算行数与实际行数是否偏差明显，再根据情况评估：",[204,11429,11431],{"className":7483,"code":11430,"language":7485,"meta":210,"style":210},"ANALYZE TABLE orders;\n",[119,11432,11433],{"__ignoreMap":210},[460,11434,11435],{"class":462,"line":463},[460,11436,11430],{},[18,11438,11439],{},"MySQL 8.0 还可以使用直方图帮助优化器理解非索引列或数据分布不均匀字段的值分布。",[18,11441,11442],{},"不要在没有验证的情况下频繁更新统计信息，也不要把所有执行计划问题都归因于统计信息。",[14,11444,11446],{"id":11445},"十二如何用-explain-判断","十二、如何用 EXPLAIN 判断",[18,11448,11449],{},"基础排查可以执行：",[204,11451,11453],{"className":7483,"code":11452,"language":7485,"meta":210,"style":210},"EXPLAIN\nSELECT *\nFROM orders\nWHERE user_id = 100;\n",[119,11454,11455,11460,11464,11468],{"__ignoreMap":210},[460,11456,11457],{"class":462,"line":463},[460,11458,11459],{},"EXPLAIN\n",[460,11461,11462],{"class":462,"line":469},[460,11463,9019],{},[460,11465,11466],{"class":462,"line":1158},[460,11467,9024],{},[460,11469,11470],{"class":462,"line":2373},[460,11471,11472],{},"WHERE user_id = 100;\n",[18,11474,11475],{},"重点观察：",[92,11477,11478,11487],{},[95,11479,11480],{},[98,11481,11482,11484],{},[101,11483,1611],{},[101,11485,11486],{},"关注点",[111,11488,11489,11498,11507,11517,11539,11549,11559],{},[98,11490,11491,11495],{},[116,11492,11493],{},[119,11494,9690],{},[116,11496,11497],{},"理论上可能使用哪些索引",[98,11499,11500,11504],{},[116,11501,11502],{},[119,11503,9701],{},[116,11505,11506],{},"优化器最终选择了哪个索引",[98,11508,11509,11514],{},[116,11510,11511],{},[119,11512,11513],{},"key_len",[116,11515,11516],{},"实际使用的索引长度，可辅助判断联合索引用到哪一部分",[98,11518,11519,11523],{},[116,11520,11521],{},[119,11522,9705],{},[116,11524,11525,11526,892,11528,892,11531,892,11534,892,11536],{},"访问方式，如 ",[119,11527,11254],{},[119,11529,11530],{},"ref",[119,11532,11533],{},"range",[119,11535,10455],{},[119,11537,11538],{},"ALL",[98,11540,11541,11546],{},[116,11542,11543],{},[119,11544,11545],{},"rows",[116,11547,11548],{},"预计需要检查的行数",[98,11550,11551,11556],{},[116,11552,11553],{},[119,11554,11555],{},"filtered",[116,11557,11558],{},"经过表条件过滤后预计保留的百分比",[98,11560,11561,11565],{},[116,11562,11563],{},[119,11564,11391],{},[116,11566,11567],{},"是否覆盖索引、ICP、额外排序或临时表等",[196,11569,11571,11572,11574],{"id":11570},"_1-type-到底表示什么","1. ",[119,11573,9705],{}," 到底表示什么",[18,11576,11577,11579,11580,11583,11584,326],{},[119,11578,9705],{}," 表示 MySQL 访问当前表中记录的方式。在 ",[119,11581,11582],{},"EXPLAIN FORMAT=JSON"," 中，对应字段名是 ",[119,11585,11586],{},"access_type",[18,11588,11589],{},"它回答的不是“有没有索引”这么简单，而是：",[57,11591,11592],{},[18,11593,11594],{},"MySQL 准备通过唯一键直接取一行、按普通索引查一组记录、扫描一个索引范围，还是扫描整棵索引或整张表？",[18,11596,11597],{},"官方文档通常按照下面的顺序描述常见访问类型：",[204,11599,11602],{"className":11600,"code":11601,"language":209,"meta":210},[207],"system\n  ↓\nconst\n  ↓\neq_ref\n  ↓\nref\n  ↓\nrange\n  ↓\nindex\n  ↓\nALL\n",[119,11603,11601],{"__ignoreMap":210},[18,11605,11606],{},"越靠前通常代表定位越精确，但这不是脱离数据量的绝对性能排名。",[18,11608,666],{},[174,11610,11611,11617,11625,11630],{},[25,11612,11613,11614,11616],{},"小表使用 ",[119,11615,11538],{}," 可能只扫描几十行，成本很低；",[25,11618,11619,11621,11622,11624],{},[119,11620,11530],{}," 虽然比 ",[119,11623,11533],{}," 靠前，但如果某个值匹配几十万行，仍然可能很慢；",[25,11626,11627,11629],{},[119,11628,11533],{}," 如果只扫描十行，通常会非常高效；",[25,11631,11632,11634],{},[119,11633,10455],{}," 虽然使用了索引，却可能把整个索引扫描一遍。",[18,11636,11637,11638,11640,11641,11644],{},"所以查看 ",[119,11639,9705],{}," 后，还必须结合 ",[119,11642,11643],{},"key、rows、filtered、Extra"," 以及实际耗时判断。",[196,11646,11648,11649,11652],{"id":11647},"_2-system表中只有一行","2. ",[119,11650,11651],{},"system","：表中只有一行",[18,11654,11655,11657,11658,11660],{},[119,11656,11651],{}," 是 ",[119,11659,11254],{}," 的特殊情况，表示优化器判断当前表只有一行。",[18,11662,11663],{},"访问过程可以理解为：",[204,11665,11668],{"className":11666,"code":11667,"language":209,"meta":210},[207],"整张表只有一行\n      ↓\n直接读取这一行\n",[119,11669,11667],{"__ignoreMap":210},[18,11671,11672,11673,11675],{},"它在普通 InnoDB 业务表的执行计划中并不常见。看到 ",[119,11674,11651],{}," 时，不需要把它理解成某种特殊索引；它表达的是表规模已经确定为单行，因此没有查找范围可言。",[196,11677,5277,11679,11681],{"id":11678},"_3-const通过主键或唯一键最多定位一行",[119,11680,11254],{},"：通过主键或唯一键最多定位一行",[18,11683,11684,11686],{},[119,11685,11254],{}," 表示当前表最多只有一条记录满足条件，并且这条记录在查询开始时只需要读取一次。",[18,11688,11689],{},"典型场景是使用常量匹配主键或完整唯一索引：",[204,11691,11693],{"className":7483,"code":11692,"language":7485,"meta":210,"style":210},"CREATE TABLE users (\n    id BIGINT PRIMARY KEY,\n    email VARCHAR(128) NOT NULL,\n    UNIQUE KEY uk_email (email)\n);\n",[119,11694,11695,11700,11705,11710,11715],{"__ignoreMap":210},[460,11696,11697],{"class":462,"line":463},[460,11698,11699],{},"CREATE TABLE users (\n",[460,11701,11702],{"class":462,"line":469},[460,11703,11704],{},"    id BIGINT PRIMARY KEY,\n",[460,11706,11707],{"class":462,"line":1158},[460,11708,11709],{},"    email VARCHAR(128) NOT NULL,\n",[460,11711,11712],{"class":462,"line":2373},[460,11713,11714],{},"    UNIQUE KEY uk_email (email)\n",[460,11716,11717],{"class":462,"line":2379},[460,11718,11719],{},");\n",[18,11721,11722],{},"查询主键：",[204,11724,11726],{"className":7483,"code":11725,"language":7485,"meta":210,"style":210},"SELECT *\nFROM users\nWHERE id = 100;\n",[119,11727,11728,11732,11736],{"__ignoreMap":210},[460,11729,11730],{"class":462,"line":463},[460,11731,9019],{},[460,11733,11734],{"class":462,"line":469},[460,11735,9301],{},[460,11737,11738],{"class":462,"line":1158},[460,11739,11740],{},"WHERE id = 100;\n",[18,11742,11743],{},"或者查询唯一键：",[204,11745,11747],{"className":7483,"code":11746,"language":7485,"meta":210,"style":210},"SELECT *\nFROM users\nWHERE email = 'tom@example.com';\n",[119,11748,11749,11753,11757],{"__ignoreMap":210},[460,11750,11751],{"class":462,"line":463},[460,11752,9019],{},[460,11754,11755],{"class":462,"line":469},[460,11756,9301],{},[460,11758,11759],{"class":462,"line":1158},[460,11760,11761],{},"WHERE email = 'tom@example.com';\n",[18,11763,11764],{},"由于主键或唯一键能够保证最多返回一行，MySQL 可以直接定位目标记录：",[204,11766,11769],{"className":11767,"code":11768,"language":209,"meta":210},[207],"常量值\n  ↓\n主键或唯一索引查找\n  ↓\n最多一条记录\n",[119,11770,11768],{"__ignoreMap":210},[18,11772,11773,11774,3213],{},"如果是联合唯一索引，需要把能够确定唯一记录的索引列完整匹配，才容易形成 ",[119,11775,11254],{},[204,11777,11779],{"className":7483,"code":11778,"language":7485,"meta":210,"style":210},"UNIQUE KEY uk_tenant_order (tenant_id, order_no)\n",[119,11780,11781],{"__ignoreMap":210},[460,11782,11783],{"class":462,"line":463},[460,11784,11778],{},[204,11786,11788],{"className":7483,"code":11787,"language":7485,"meta":210,"style":210},"WHERE tenant_id = 10\n  AND order_no = 'A1001'\n",[119,11789,11790,11795],{"__ignoreMap":210},[460,11791,11792],{"class":462,"line":463},[460,11793,11794],{},"WHERE tenant_id = 10\n",[460,11796,11797],{"class":462,"line":469},[460,11798,11799],{},"  AND order_no = 'A1001'\n",[18,11801,11802],{},"如果只查询：",[204,11804,11805],{"className":7483,"code":11794,"language":7485,"meta":210,"style":210},[119,11806,11807],{"__ignoreMap":210},[460,11808,11809],{"class":462,"line":463},[460,11810,11794],{},[18,11812,11813,11814,326],{},"可能返回多行，通常就不是 ",[119,11815,11254],{},[18,11817,11818,11820],{},[119,11819,11254],{}," 的核心不是“使用了等号”，而是：",[57,11822,11823],{},[18,11824,11825],{},"通过常量和唯一约束，能够确定当前表最多只返回一条记录。",[196,11827,11829,11830,11833],{"id":11828},"_4-eq_refjoin-时每条前表记录最多匹配一行","4. ",[119,11831,11832],{},"eq_ref","：JOIN 时每条前表记录最多匹配一行",[18,11835,11836,11838],{},[119,11837,11832],{}," 最常见于多表 JOIN。",[18,11840,11841,11842,11845],{},"它表示对于前面表产生的每一条记录，MySQL 都通过主键或 ",[119,11843,11844],{},"UNIQUE NOT NULL"," 索引，在当前表中最多查到一条记录。",[18,11847,666],{},[204,11849,11851],{"className":7483,"code":11850,"language":7485,"meta":210,"style":210},"CREATE TABLE orders (\n    id BIGINT PRIMARY KEY,\n    user_id BIGINT NOT NULL,\n    KEY idx_user_id (user_id)\n);\n",[119,11852,11853,11858,11862,11867,11872],{"__ignoreMap":210},[460,11854,11855],{"class":462,"line":463},[460,11856,11857],{},"CREATE TABLE orders (\n",[460,11859,11860],{"class":462,"line":469},[460,11861,11704],{},[460,11863,11864],{"class":462,"line":1158},[460,11865,11866],{},"    user_id BIGINT NOT NULL,\n",[460,11868,11869],{"class":462,"line":2373},[460,11870,11871],{},"    KEY idx_user_id (user_id)\n",[460,11873,11874],{"class":462,"line":2379},[460,11875,11719],{},[204,11877,11879],{"className":7483,"code":11878,"language":7485,"meta":210,"style":210},"SELECT o.id, u.name\nFROM orders o\nJOIN users u ON u.id = o.user_id;\n",[119,11880,11881,11885,11889],{"__ignoreMap":210},[460,11882,11883],{"class":462,"line":463},[460,11884,11224],{},[460,11886,11887],{"class":462,"line":469},[460,11888,11229],{},[460,11890,11891],{"class":462,"line":1158},[460,11892,11893],{},"JOIN users u ON u.id = o.user_id;\n",[18,11895,11896,11897,11899,11900,9622,11903,3213],{},"假设执行顺序是先读取 ",[119,11898,11250],{},"，再根据 ",[119,11901,11902],{},"o.user_id",[119,11904,11274],{},[204,11906,11909],{"className":11907,"code":11908,"language":209,"meta":210},[207],"orders 的一条记录\n        ↓\nu.id = o.user_id\n        ↓\n通过 users 主键最多定位一行\n",[119,11910,11908],{"__ignoreMap":210},[18,11912,11385,11913,11915,11916,326],{},[119,11914,11274],{}," 表的访问类型可能是 ",[119,11917,11832],{},[18,11919,11920,2084,11922,11924],{},[119,11921,11254],{},[119,11923,11832],{}," 的区别是：",[92,11926,11927,11939],{},[95,11928,11929],{},[98,11930,11931,11934,11937],{},[101,11932,11933],{},"类型",[101,11935,11936],{},"查找值来自哪里",[101,11938,109],{},[111,11940,11941,11955],{},[98,11942,11943,11947,11950],{},[116,11944,11945],{},[119,11946,11254],{},[116,11948,11949],{},"SQL 中的常量",[116,11951,11952],{},[119,11953,11954],{},"WHERE id = 100",[98,11956,11957,11961,11964],{},[116,11958,11959],{},[119,11960,11832],{},[116,11962,11963],{},"前面表的列值",[116,11965,11966],{},[119,11967,11968],{},"u.id = o.user_id",[196,11970,4015,11972,11974],{"id":11971},"_5-ref通过非唯一索引等值查找一组记录",[119,11973,11530],{},"：通过非唯一索引等值查找一组记录",[18,11976,11977,11979],{},[119,11978,11530],{}," 表示使用普通索引或联合索引的非唯一前缀进行等值查找，可能返回多条具有相同索引值的记录。",[18,11981,666],{},[204,11983,11984],{"className":7483,"code":10553,"language":7485,"meta":210,"style":210},[119,11985,11986],{"__ignoreMap":210},[460,11987,11988],{"class":462,"line":463},[460,11989,10553],{},[204,11991,11992],{"className":7483,"code":10895,"language":7485,"meta":210,"style":210},[119,11993,11994,11998,12002],{"__ignoreMap":210},[460,11995,11996],{"class":462,"line":463},[460,11997,9019],{},[460,11999,12000],{"class":462,"line":469},[460,12001,9024],{},[460,12003,12004],{"class":462,"line":1158},[460,12005,10910],{},[18,12007,3234,12008,12011],{},[119,12009,12010],{},"status=1"," 对应多条订单，访问过程是：",[204,12013,12016],{"className":12014,"code":12015,"language":209,"meta":210},[207],"在 idx_status 中定位 status=1 的起点\n                  ↓\n连续读取所有 status=1 的索引记录\n                  ↓\n按需回表\n",[119,12017,12015],{"__ignoreMap":210},[18,12019,12020,12021,12023],{},"因此，",[119,12022,11530],{}," 不代表只读取一行。它可能读取几行，也可能读取几十万行，实际成本取决于索引选择性和回表数量。",[18,12025,12026,12027,3213],{},"联合索引只使用左侧一部分时，也可能出现 ",[119,12028,11530],{},[204,12030,12032],{"className":7483,"code":12031,"language":7485,"meta":210,"style":210},"KEY idx_tenant_status (tenant_id, status)\n",[119,12033,12034],{"__ignoreMap":210},[460,12035,12036],{"class":462,"line":463},[460,12037,12031],{},[204,12039,12040],{"className":7483,"code":11794,"language":7485,"meta":210,"style":210},[119,12041,12042],{"__ignoreMap":210},[460,12043,12044],{"class":462,"line":463},[460,12045,11794],{},[18,12047,12048],{},"虽然使用了索引最左列，但同一个租户可能有大量记录，所以仍然是非唯一匹配。",[18,12050,12051,12052,3213],{},"JOIN 中如果使用普通索引关联，也常见 ",[119,12053,11530],{},[204,12055,12057],{"className":7483,"code":12056,"language":7485,"meta":210,"style":210},"SELECT *\nFROM users u\nJOIN orders o ON o.user_id = u.id;\n",[119,12058,12059,12063,12068],{"__ignoreMap":210},[460,12060,12061],{"class":462,"line":463},[460,12062,9019],{},[460,12064,12065],{"class":462,"line":469},[460,12066,12067],{},"FROM users u\n",[460,12069,12070],{"class":462,"line":1158},[460,12071,12072],{},"JOIN orders o ON o.user_id = u.id;\n",[18,12074,12075,12076,12079,12080,12082,12083,12085,12086,326],{},"对于每个用户，普通索引 ",[119,12077,12078],{},"orders.idx_user_id"," 可能匹配多条订单，因此 ",[119,12081,11250],{}," 的访问类型可能是 ",[119,12084,11530],{},"，而不是 ",[119,12087,11832],{},[196,12089,12091,12092,12094],{"id":12090},"_6-range扫描索引中的一个或多个区间","6. ",[119,12093,11533],{},"：扫描索引中的一个或多个区间",[18,12096,12097,12099],{},[119,12098,11533],{}," 表示 MySQL 先根据条件确定一个或多个索引区间，然后只扫描这些区间中的记录。",[18,12101,12102],{},"典型范围查询：",[204,12104,12106],{"className":7483,"code":12105,"language":7485,"meta":210,"style":210},"SELECT *\nFROM orders\nWHERE created_at >= '2026-07-01'\n  AND created_at \u003C  '2026-08-01';\n",[119,12107,12108,12112,12116,12121],{"__ignoreMap":210},[460,12109,12110],{"class":462,"line":463},[460,12111,9019],{},[460,12113,12114],{"class":462,"line":469},[460,12115,9024],{},[460,12117,12118],{"class":462,"line":1158},[460,12119,12120],{},"WHERE created_at >= '2026-07-01'\n",[460,12122,12123],{"class":462,"line":2373},[460,12124,12125],{},"  AND created_at \u003C  '2026-08-01';\n",[18,12127,12128],{},"索引访问过程可以理解为：",[204,12130,12133],{"className":12131,"code":12132,"language":209,"meta":210},[207],"定位 2026-07-01 的索引位置\n              ↓\n沿叶子节点向后扫描\n              ↓\n到达 2026-08-01 时停止\n",[119,12134,12132],{"__ignoreMap":210},[18,12136,12137,12138,12140],{},"常见能够形成 ",[119,12139,11533],{}," 的条件包括：",[174,12142,12143,12153,12157,12163,12167,12172],{},[25,12144,12145,892,12147,892,12149,892,12151,1426],{},[119,12146,9064],{},[119,12148,9067],{},[119,12150,9070],{},[119,12152,9073],{},[25,12154,12155,1426],{},[119,12156,9078],{},[25,12158,12159,12162],{},[119,12160,12161],{},"IN (...)"," 形成多个离散范围；",[25,12164,12165,1426],{},[119,12166,10884],{},[25,12168,12169,12170,1426],{},"能确定前缀范围的 ",[119,12171,9084],{},[25,12173,12174,12175,12178],{},"某些 ",[119,12176,12177],{},"\u003C>"," 或其他可转化为索引区间的条件。",[18,12180,666],{},[204,12182,12184],{"className":7483,"code":12183,"language":7485,"meta":210,"style":210},"WHERE id IN (10, 20, 30)\n",[119,12185,12186],{"__ignoreMap":210},[460,12187,12188],{"class":462,"line":463},[460,12189,12183],{},[18,12191,12192,12193,12195,12196,326],{},"虽然使用的是多个等值条件，优化器也可能把它组织成多个索引范围，因此 ",[119,12194,9705],{}," 显示为 ",[119,12197,11533],{},[18,12199,12200],{},"具体来说，范围优化器可以把：",[204,12202,12203],{"className":7483,"code":12183,"language":7485,"meta":210,"style":210},[119,12204,12205],{"__ignoreMap":210},[460,12206,12207],{"class":462,"line":463},[460,12208,12183],{},[18,12210,12211],{},"转换为同一个索引上的三个单点区间：",[204,12213,12216],{"className":12214,"code":12215,"language":209,"meta":210},[207],"[10, 10]\n∪\n[20, 20]\n∪\n[30, 30]\n",[119,12217,12215],{"__ignoreMap":210},[18,12219,12220,12221,12223,12224,12227],{},"执行时并不是从 ",[119,12222,1522],{}," 一直扫描到 ",[119,12225,12226],{},"30","，而是分别在同一棵 B+ 树中定位这些单点范围，再合并访问结果：",[204,12229,12232],{"className":12230,"code":12231,"language":209,"meta":210},[207],"定位 id=10\n定位 id=20\n定位 id=30\n     ↓\n返回三个范围的记录\n",[119,12233,12231],{"__ignoreMap":210},[18,12235,12236,12237,12239,12240,326],{},"因此，SQL 语义上虽然是多个等值条件，物理访问方式仍属于“扫描一个或多个索引范围”，",[119,12238,9705],{}," 可以显示为 ",[119,12241,11533],{},[18,12243,12244],{},"这与 Index Merge 不同：",[174,12246,12247,12257],{},[25,12248,12249,12252,12253,12256],{},[119,12250,12251],{},"IN (10,20,30)"," 通常是在",[28,12254,12255],{},"同一个索引","上构造多个范围；",[25,12258,12259,12260,12263],{},"Index Merge 是扫描",[28,12261,12262],{},"同一张表的多个索引","，再合并结果。",[18,12265,12266,12268],{},[119,12267,11533],{}," 是否高效，主要看范围大小：",[204,12270,12273],{"className":12271,"code":12272,"language":209,"meta":210},[207],"扫描 10 条索引记录       → 通常很快\n扫描 80 万条索引记录     → 仍可能很慢\n",[119,12274,12272],{"__ignoreMap":210},[18,12276,12277,12278,12280],{},"所以不能看到 ",[119,12279,11533],{}," 就直接认定 SQL 已经优化完成。",[196,12282,12284],{"id":12283},"_7-icp在回表前用索引中的字段过滤","7. ICP：在回表前用索引中的字段过滤",[18,12286,12287],{},"ICP 全称是 Index Condition Pushdown，中文通常称为“索引条件下推”。",[18,12289,12290],{},"仍然使用联合索引：",[204,12292,12293],{"className":7483,"code":9788,"language":7485,"meta":210,"style":210},[119,12294,12295],{"__ignoreMap":210},[460,12296,12297],{"class":462,"line":463},[460,12298,9788],{},[18,12300,9797],{},[204,12302,12303],{"className":7483,"code":9800,"language":7485,"meta":210,"style":210},[119,12304,12305,12309,12313,12317,12321],{"__ignoreMap":210},[460,12306,12307],{"class":462,"line":463},[460,12308,9019],{},[460,12310,12311],{"class":462,"line":469},[460,12312,9811],{},[460,12314,12315],{"class":462,"line":1158},[460,12316,9816],{},[460,12318,12319],{"class":462,"line":2373},[460,12320,9821],{},[460,12322,12323],{"class":462,"line":2379},[460,12324,9826],{},[18,12326,12327,12329,12330,12332],{},[119,12328,9832],{}," 可以确定索引扫描范围，但 ",[119,12331,9840],{}," 通常不能继续缩小这个连续范围。",[18,12334,12335],{},"没有 ICP 时，过程近似为：",[204,12337,12340],{"className":12338,"code":12339,"language":209,"meta":210},[207],"扫描满足 a=1、b>10 的二级索引记录\n                ↓\n每一条都根据主键回表\n                ↓\n取得完整行后判断 c=3\n",[119,12341,12339],{"__ignoreMap":210},[18,12343,12344,12345,12347,12348,3213],{},"开启 ICP 后，由于 ",[119,12346,9633],{}," 也存储在联合索引中，存储引擎可以先检查索引记录中的 ",[119,12349,9633],{},[204,12351,12354],{"className":12352,"code":12353,"language":209,"meta":210},[207],"扫描满足 a=1、b>10 的二级索引记录\n                ↓\n直接在索引记录中判断 c=3\n        ├─ 不满足：跳过，不回表\n        └─ 满足：根据主键回表\n",[119,12355,12353],{"__ignoreMap":210},[18,12357,12358],{},"它的主要收益是：",[57,12360,12361],{},[18,12362,12363],{},"减少不必要的聚簇索引回表，而不是进一步缩小最初的索引扫描范围。",[18,12365,10589,12366,11388,12368,12370],{},[119,12367,9694],{},[119,12369,11391],{}," 中通常显示：",[204,12372,12375],{"className":12373,"code":12374,"language":209,"meta":210},[207],"Using index condition\n",[119,12376,12374],{"__ignoreMap":210},[18,12378,12379],{},"ICP 与覆盖索引也不同：",[174,12381,12382,12385],{},[25,12383,12384],{},"ICP：仍可能需要回表，只是先在索引层过滤；",[25,12386,12387,12388,12390,12391,326],{},"覆盖索引：查询所需数据全部在索引中，最终也不需要回表，",[119,12389,11391],{}," 常见 ",[119,12392,12393],{},"Using index",[18,12395,12396],{},"只有能够利用当前索引记录判断的条件，才适合下推到存储引擎；依赖其他表、存储函数或无法从索引记录取得的数据，通常不能这样处理。",[196,12398,12400,12401,12403],{"id":12399},"_8-index把整棵索引扫描一遍","8. ",[119,12402,10455],{},"：把整棵索引扫描一遍",[18,12405,12406,12408],{},[119,12407,10455],{}," 很容易被误解为“查询已经通过索引精准定位”。",[18,12410,12411,12412,12415],{},"实际上，",[119,12413,12414],{},"type=index"," 通常表示：",[57,12417,12418],{},[18,12419,12420],{},"MySQL 没有通过索引条件缩小扫描区间，而是把选中的整棵索引从头到尾扫描一遍。",[18,12422,12423,12424,12426],{},"它与 ",[119,12425,11538],{}," 的共同点是都可能读取全部记录；区别在于：",[174,12428,12429,12434],{},[25,12430,12431,12433],{},[119,12432,10455],{}," 扫描索引树；",[25,12435,12436,12438],{},[119,12437,11538],{}," 扫描表数据。",[18,12440,666],{},[204,12442,12444],{"className":7483,"code":12443,"language":7485,"meta":210,"style":210},"KEY idx_user_id (user_id)\n",[119,12445,12446],{"__ignoreMap":210},[460,12447,12448],{"class":462,"line":463},[460,12449,12443],{},[204,12451,12453],{"className":7483,"code":12452,"language":7485,"meta":210,"style":210},"SELECT user_id\nFROM orders;\n",[119,12454,12455,12460],{"__ignoreMap":210},[460,12456,12457],{"class":462,"line":463},[460,12458,12459],{},"SELECT user_id\n",[460,12461,12462],{"class":462,"line":469},[460,12463,12464],{},"FROM orders;\n",[18,12466,12467,12468,12470,12471,12474],{},"如果查询只需要 ",[119,12469,10466],{},"，二级索引已经包含全部所需数据，MySQL 可以扫描更窄的 ",[119,12472,12473],{},"idx_user_id","，而不必扫描保存完整行的聚簇索引。",[18,12476,12477],{},"执行计划可能显示：",[204,12479,12482],{"className":12480,"code":12481,"language":209,"meta":210},[207],"type: index\nkey: idx_user_id\nExtra: Using index\n",[119,12483,12481],{"__ignoreMap":210},[18,12485,7325,12486,12488],{},[119,12487,12393],{}," 表示覆盖索引：查询需要的数据可以直接从索引取得，不需要回表。",[18,12490,12491],{},"另一个常见场景是利用索引顺序避免额外排序：",[204,12493,12495],{"className":7483,"code":12494,"language":7485,"meta":210,"style":210},"SELECT id\nFROM orders\nORDER BY created_at;\n",[119,12496,12497,12501,12505],{"__ignoreMap":210},[460,12498,12499],{"class":462,"line":463},[460,12500,9333],{},[460,12502,12503],{"class":462,"line":469},[460,12504,9024],{},[460,12506,12507],{"class":462,"line":1158},[460,12508,10997],{},[18,12510,12511,12512,12514,12515,326],{},"如果存在适合的 ",[119,12513,10065],{}," 索引，优化器可能按索引顺序扫描全部记录，以避免 ",[119,12516,11165],{},[18,12518,12519,12521,12522,12524],{},[119,12520,10455],{}," 可能比 ",[119,12523,11538],{}," 快，因为二级索引通常更窄，缓存命中和读取的数据量更小；但它本质上仍是全索引扫描，数据量大时成本依然很高。",[196,12526,12528,12529,12531],{"id":12527},"_9-all扫描整张表","9. ",[119,12530,11538],{},"：扫描整张表",[18,12533,12534,12536],{},[119,12535,11538],{}," 表示全表扫描。",[18,12538,12539],{},"对于 InnoDB，可以把它近似理解为扫描保存完整行数据的聚簇索引：",[204,12541,12544],{"className":12542,"code":12543,"language":209,"meta":210},[207],"聚簇索引第一条记录\n        ↓\n逐条向后读取\n        ↓\n检查 WHERE 条件\n        ↓\n直到最后一条记录\n",[119,12545,12543],{"__ignoreMap":210},[18,12547,12548],{},"典型原因包括：",[174,12550,12551,12554,12557,12561,12564,12567],{},[25,12552,12553],{},"查询条件没有可用索引；",[25,12555,12556],{},"在索引列上进行了导致无法定位的函数或转换；",[25,12558,12559,9660],{},[119,12560,9614],{},[25,12562,12563],{},"条件会返回表中大部分数据；",[25,12565,12566],{},"表很小，全表扫描成本更低；",[25,12568,12569],{},"优化器根据统计信息判断使用索引加回表更贵。",[18,12571,666],{},[204,12573,12575],{"className":7483,"code":12574,"language":7485,"meta":210,"style":210},"SELECT *\nFROM orders\nWHERE remark LIKE '%manual%';\n",[119,12576,12577,12581,12585],{"__ignoreMap":210},[460,12578,12579],{"class":462,"line":463},[460,12580,9019],{},[460,12582,12583],{"class":462,"line":469},[460,12584,9024],{},[460,12586,12587],{"class":462,"line":1158},[460,12588,12589],{},"WHERE remark LIKE '%manual%';\n",[18,12591,12592,12593,326],{},"普通 B+ 树索引无法按照任意位置包含关系定位，执行计划很可能是 ",[119,12594,11538],{},[18,12596,12597,12598,12600],{},"但 ",[119,12599,11538],{}," 不等于必须立刻创建索引。",[18,12602,12603],{},"如果表只有几十行，或者查询本来就要读取 80% 的数据，全表扫描可能是合理选择。真正需要关注的是：",[174,12605,12606,12609,12614,12617,12620],{},[25,12607,12608],{},"表有多大；",[25,12610,12611,12613],{},[119,12612,11545],{}," 预计扫描多少行；",[25,12615,12616],{},"查询执行频率多高；",[25,12618,12619],{},"是否处于 JOIN 的内层并被重复扫描；",[25,12621,12622],{},"实际耗时和资源消耗是否不可接受。",[18,12624,12625,12626,12628],{},"特别是在 JOIN 中，如果前表返回一万行，而后表对每条前表记录都执行一次 ",[119,12627,11538],{},"，成本可能被放大得非常严重。",[196,12630,12632],{"id":12631},"_10-常见类型对比","10. 常见类型对比",[92,12634,12635,12655],{},[95,12636,12637],{},[98,12638,12639,12643,12646,12649,12652],{},[101,12640,12641],{},[119,12642,9705],{},[101,12644,12645],{},"核心含义",[101,12647,12648],{"align":7229},"是否精准定位",[101,12650,12651],{"align":7229},"可能读取的记录数",[101,12653,12654],{},"常见场景",[111,12656,12657,12675,12695,12715,12738,12756],{},[98,12658,12659,12663,12666,12669,12672],{},[116,12660,12661],{},[119,12662,11651],{},[116,12664,12665],{},"表中只有一行",[116,12667,12668],{"align":7229},"无需查找",[116,12670,12671],{"align":7229},"1 行",[116,12673,12674],{},"单行表",[98,12676,12677,12681,12684,12687,12690],{},[116,12678,12679],{},[119,12680,11254],{},[116,12682,12683],{},"通过常量匹配主键或唯一键",[116,12685,12686],{"align":7229},"是",[116,12688,12689],{"align":7229},"最多 1 行",[116,12691,12692],{},[119,12693,12694],{},"WHERE id=100",[98,12696,12697,12701,12704,12707,12710],{},[116,12698,12699],{},[119,12700,11530],{},[116,12702,12703],{},"通过普通索引做等值查找",[116,12705,12706],{"align":7229},"是，但结果不唯一",[116,12708,12709],{"align":7229},"1 行到大量记录",[116,12711,12712],{},[119,12713,12714],{},"WHERE status=1",[98,12716,12717,12721,12724,12727,12730],{},[116,12718,12719],{},[119,12720,11533],{},[116,12722,12723],{},"扫描一个或多个索引区间",[116,12725,12726],{"align":7229},"定位区间",[116,12728,12729],{"align":7229},"取决于范围大小",[116,12731,12732,892,12734,892,12736],{},[119,12733,9078],{},[119,12735,10052],{},[119,12737,9064],{},[98,12739,12740,12744,12747,12750,12753],{},[116,12741,12742],{},[119,12743,10455],{},[116,12745,12746],{},"扫描整棵索引",[116,12748,12749],{"align":7229},"否",[116,12751,12752],{"align":7229},"通常为整个索引",[116,12754,12755],{},"覆盖索引全扫描、按索引顺序遍历",[98,12757,12758,12762,12765,12767,12770],{},[116,12759,12760],{},[119,12761,11538],{},[116,12763,12764],{},"扫描整张表",[116,12766,12749],{"align":7229},[116,12768,12769],{"align":7229},"通常为整张表",[116,12771,12772],{},"无可用索引或全表扫描更便宜",[18,12774,12775,12776,3213],{},"还应单独记住 JOIN 中的 ",[119,12777,11832],{},[57,12779,12780],{},[18,12781,12782],{},"对前表的每一条记录，通过当前表的主键或唯一非空索引最多匹配一行。",[196,12784,12786],{"id":12785},"_11-explain-与-explain-analyze-的区别","11. EXPLAIN 与 EXPLAIN ANALYZE 的区别",[18,12788,12789,12791,12792,3213],{},[119,12790,9694],{}," 只展示优化器计划如何执行 SQL，并不会真正执行普通 ",[119,12793,7169],{},[204,12795,12796],{"className":7483,"code":11452,"language":7485,"meta":210,"style":210},[119,12797,12798,12802,12806,12810],{"__ignoreMap":210},[460,12799,12800],{"class":462,"line":463},[460,12801,11459],{},[460,12803,12804],{"class":462,"line":469},[460,12805,9019],{},[460,12807,12808],{"class":462,"line":1158},[460,12809,9024],{},[460,12811,12812],{"class":462,"line":2373},[460,12813,11472],{},[18,12815,12816,12817,12820,12821,326],{},"其中的 ",[119,12818,12819],{},"rows、filtered、cost"," 等主要来自统计信息和成本模型，是",[28,12822,12823],{},"估算值",[18,12825,12826,12828],{},[119,12827,9697],{}," 会真正执行查询，并在执行计划中同时展示估算值和实际运行数据：",[204,12830,12832],{"className":7483,"code":12831,"language":7485,"meta":210,"style":210},"EXPLAIN ANALYZE\nSELECT *\nFROM orders\nWHERE user_id = 100;\n",[119,12833,12834,12839,12843,12847],{"__ignoreMap":210},[460,12835,12836],{"class":462,"line":463},[460,12837,12838],{},"EXPLAIN ANALYZE\n",[460,12840,12841],{"class":462,"line":469},[460,12842,9019],{},[460,12844,12845],{"class":462,"line":1158},[460,12846,9024],{},[460,12848,12849],{"class":462,"line":2373},[460,12850,11472],{},[18,12852,12853],{},"典型输出片段：",[204,12855,12858],{"className":12856,"code":12857,"language":209,"meta":210},[207],"Index lookup on orders using idx_user_id\n  (cost=10.2 rows=20)\n  (actual time=0.08..0.15 rows=350 loops=1)\n",[119,12859,12857],{"__ignoreMap":210},[18,12861,12862],{},"可以重点对比：",[174,12864,12865,12871,12877,12883,12889],{},[25,12866,12867,12870],{},[119,12868,12869],{},"cost","：优化器估算成本；",[25,12872,12873,12874,12876],{},"前一个 ",[119,12875,11545],{},"：预计返回行数；",[25,12878,12879,12882],{},[119,12880,12881],{},"actual time","：实际首行时间和完成时间；",[25,12884,12885,12886,12888],{},"后一个 ",[119,12887,11545],{},"：每次循环实际返回行数；",[25,12890,12891,12894],{},[119,12892,12893],{},"loops","：该执行节点实际执行次数。",[18,12896,12897],{},"两者的核心区别：",[92,12899,12900,12914],{},[95,12901,12902],{},[98,12903,12904,12906,12910],{},[101,12905,2756],{},[101,12907,12908],{},[119,12909,9694],{},[101,12911,12912],{},[119,12913,9697],{},[111,12915,12916,12929,12940,12951,12962],{},[98,12917,12918,12921,12926],{},[116,12919,12920],{},"是否真正执行查询",[116,12922,7166,12923,12925],{},[119,12924,7169],{}," 不执行",[116,12927,12928],{},"会真实执行",[98,12930,12931,12934,12937],{},[116,12932,12933],{},"行数与成本",[116,12935,12936],{},"主要是估算值",[116,12938,12939],{},"同时提供估算与实际数据",[98,12941,12942,12945,12948],{},[116,12943,12944],{},"实际耗时",[116,12946,12947],{},"不提供",[116,12949,12950],{},"提供各执行节点耗时",[98,12952,12953,12956,12959],{},[116,12954,12955],{},"主要用途",[116,12957,12958],{},"上线前检查计划、低风险分析",[116,12960,12961],{},"验证估算偏差、定位真实瓶颈",[98,12963,12964,12967,12970],{},[116,12965,12966],{},"使用风险",[116,12968,12969],{},"较低",[116,12971,12972],{},"慢 SQL 会真实消耗资源",[18,12974,12975,12977],{},[119,12976,9697],{}," 能发现这类问题：",[204,12979,12982],{"className":12980,"code":12981,"language":209,"meta":210},[207],"优化器预计返回 20 行\n实际返回 350000 行\n",[119,12983,12981],{"__ignoreMap":210},[18,12985,12986],{},"这种巨大偏差可能与统计信息、数据倾斜、条件相关性或估算模型有关。",[18,12988,12989],{},"使用时必须注意：",[174,12991,12992,12995,12998,13001],{},[25,12993,12994],{},"它会真实执行查询，慢查询会真实占用 CPU、I\u002FO 和锁资源；",[25,12996,12997],{},"对复杂 SQL 应优先在测试或影子环境执行；",[25,12999,13000],{},"不要在生产高峰期随意分析可能扫描海量数据的语句；",[25,13002,13003],{},"对数据修改语句尤其谨慎，不能把它当成完全无副作用的静态分析工具。",[196,13005,13007,13008,13010],{"id":13006},"_12-不要只根据-type-判断性能","12. 不要只根据 ",[119,13009,9705],{}," 判断性能",[18,13012,13013],{},"下面两个计划：",[204,13015,13018],{"className":13016,"code":13017,"language":209,"meta":210},[207],"计划 A：type=ref，rows=500000\n计划 B：type=range，rows=20\n",[119,13019,13017],{"__ignoreMap":210},[18,13021,13022,13023,13025,13026,13028],{},"虽然官方列表中 ",[119,13024,11530],{}," 排在 ",[119,13027,11533],{}," 前面，但计划 B 很可能扫描更少、执行更快。",[18,13030,13031],{},"同理：",[204,13033,13036],{"className":13034,"code":13035,"language":209,"meta":210},[207],"计划 A：type=ALL，rows=30\n计划 B：type=ref，rows=100000\n",[119,13037,13035],{"__ignoreMap":210},[18,13039,13040],{},"对只有 30 行的小表执行全表扫描，不一定是问题。",[18,13042,13043],{},"因此，合理的判断方式是：",[204,13045,13048],{"className":13046,"code":13047,"language":209,"meta":210},[207],"type\n  +\nkey \u002F key_len\n  +\nrows \u002F filtered\n  +\nExtra\n  +\nEXPLAIN ANALYZE 的实际行数与耗时\n",[119,13049,13047],{"__ignoreMap":210},[18,13051,13052,13054],{},[119,13053,9705],{}," 是执行计划的重要入口，但不是最终结论。",[18,13056,13057],{},"几个常见误区：",[174,13059,13060,13065,13070,13075,13081,13086],{},[25,13061,13062,13064],{},[119,13063,9690],{}," 有值，不代表最终使用了索引；",[25,13066,13067,13069],{},[119,13068,9701],{}," 有值，不代表查询一定高效；",[25,13071,13072,13074],{},[119,13073,12414],{}," 通常表示全索引扫描，不等于范围定位；",[25,13076,13077,13080],{},[119,13078,13079],{},"type=ALL"," 是全表扫描，但小表全表扫描可能就是合理计划；",[25,13082,13083,13085],{},[119,13084,12393],{}," 通常表示覆盖索引，不等于“使用索引定位”；",[25,13087,13088],{},"预计扫描行数少，也可能因估算偏差与实际执行差异很大。",[14,13090,13092],{"id":13091},"十三排查顺序","十三、排查顺序",[18,13094,13095],{},"遇到“明明建了索引却很慢”，可以按下面顺序检查：",[22,13097,13098,13101,13104,13119,13122,13125,13128,13131,13137],{},[25,13099,13100],{},"确认慢 SQL、参数值和数据量，不能只拿脱离现场的 SQL 模板分析。",[25,13102,13103],{},"查看表结构、索引顺序、字段类型、字符集和排序规则。",[25,13105,3075,13106,13108,13109,892,13111,892,13113,892,13115,2084,13117,326],{},[119,13107,9694],{}," 检查 ",[119,13110,9701],{},[119,13112,9705],{},[119,13114,11545],{},[119,13116,11555],{},[119,13118,11391],{},[25,13120,13121],{},"检查联合索引是否满足最左前缀，范围条件之后是否只剩过滤能力。",[25,13123,13124],{},"检查索引列上是否存在函数、计算或隐式转换。",[25,13126,13127],{},"估算查询命中比例和回表次数，判断优化器放弃索引是否合理。",[25,13129,13130],{},"对比估算行数与实际行数，检查统计信息是否明显失真。",[25,13132,13133,13134,13136],{},"在可控环境中使用 ",[119,13135,9697],{}," 验证真实执行路径。",[25,13138,13139,13140,13142],{},"调整 SQL 或索引后，在可控环境中使用 ",[119,13141,9697],{}," 验证实际执行情况，并结合线上灰度后的查询耗时、扫描行数、回表量及资源指标持续观察，不能只看静态执行计划就宣布优化完成。只有涉及高并发、容量上限或锁竞争的场景，才需要进一步进行专项压测。",[14,13144,13146],{"id":13145},"十四常见错误结论","十四、常见错误结论",[196,13148,13150],{"id":13149},"_1-联合索引中间断了一列整个索引都失效","1. 联合索引中间断了一列，整个索引都失效",[18,13152,13153],{},"不准确。最左侧连续部分仍可能用于定位，后续列也可能通过 ICP 或覆盖索引发挥作用。",[196,13155,13157],{"id":13156},"_2-遇到范围查询后面的索引列完全没用","2. 遇到范围查询，后面的索引列完全没用",[18,13159,13160],{},"不准确。后续列通常不能继续缩小连续范围，但仍可能用于索引层过滤或覆盖查询。",[196,13162,5277,13164,892,13166,892,13168,13170],{"id":13163},"_3-not-inis-null-一定不走索引",[119,13165,9671],{},[119,13167,9674],{},[119,13169,10884],{}," 一定不走索引",[18,13172,13173],{},"不准确。是否选择索引取决于数据分布、命中比例、覆盖情况和优化器成本估算。",[196,13175,11829,13177,13179],{"id":13176},"_4-key-不为-null-就说明索引使用良好",[119,13178,9701],{}," 不为 NULL 就说明索引使用良好",[18,13181,13182],{},"不准确。可能扫描了大部分索引，也可能发生大量回表。",[196,13184,13186],{"id":13185},"_5-全表扫描一定比索引慢","5. 全表扫描一定比索引慢",[18,13188,13189],{},"不准确。小表或需要读取大部分数据时，顺序全表扫描可能是成本更低的方案。",[196,13191,13193,13194,13196],{"id":13192},"_6-使用-force-index-就能解决问题","6. 使用 ",[119,13195,10951],{}," 就能解决问题",[18,13198,13199],{},"不准确。强制索引可能暂时改变执行计划，也可能在数据分布变化后变得更慢。应先找到优化器不选择索引的真实原因。",[14,13201,13203],{"id":13202},"十五参考资料","十五、参考资料",[174,13205,13206,13213,13220,13227,13234,13241,13248,13255],{},[25,13207,13208],{},[70,13209,13212],{"href":13210,"rel":13211},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.0\u002Fen\u002Fmysql-indexes.html",[1101],"MySQL 8.0 Reference Manual：How MySQL Uses Indexes",[25,13214,13215],{},[70,13216,13219],{"href":13217,"rel":13218},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.0\u002Fen\u002Foptimization-indexes.html",[1101],"MySQL 8.0 Reference Manual：Optimization and Indexes",[25,13221,13222],{},[70,13223,13226],{"href":13224,"rel":13225},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.0\u002Fen\u002Fexplain.html",[1101],"MySQL 8.0 Reference Manual：EXPLAIN Statement",[25,13228,13229],{},[70,13230,13233],{"href":13231,"rel":13232},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.0\u002Fen\u002Findex-merge-optimization.html",[1101],"MySQL 8.0 Reference Manual：Index Merge Optimization",[25,13235,13236],{},[70,13237,13240],{"href":13238,"rel":13239},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.0\u002Fen\u002Findex-condition-pushdown-optimization.html",[1101],"MySQL 8.0 Reference Manual：Index Condition Pushdown Optimization",[25,13242,13243],{},[70,13244,13247],{"href":13245,"rel":13246},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.0\u002Fen\u002Forder-by-optimization.html",[1101],"MySQL 8.0 Reference Manual：ORDER BY Optimization",[25,13249,13250],{},[70,13251,13254],{"href":13252,"rel":13253},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.0\u002Fen\u002Fcreate-index.html",[1101],"MySQL 8.0 Reference Manual：CREATE INDEX Statement",[25,13256,13257],{},[70,13258,13261],{"href":13259,"rel":13260},"https:\u002F\u002Fdev.mysql.com\u002Fdoc\u002Frefman\u002F8.0\u002Fen\u002Fgenerated-column-index-optimizations.html",[1101],"MySQL 8.0 Reference Manual：Optimizer Use of Generated Column Indexes",[1146,13263,1148],{},{"title":210,"searchDepth":469,"depth":469,"links":13265},[13266,13269,13270,13271,13276,13277,13278,13279,13280,13281,13287,13288,13289,13294,13295,13318,13319,13330],{"id":16,"depth":469,"text":16,"children":13267},[13268],{"id":7181,"depth":1158,"text":7181},{"id":66,"depth":469,"text":66},{"id":83,"depth":469,"text":83},{"id":9729,"depth":469,"text":9730,"children":13272},[13273,13274,13275],{"id":9736,"depth":1158,"text":9737},{"id":9779,"depth":1158,"text":9780},{"id":9853,"depth":1158,"text":9854},{"id":9872,"depth":469,"text":9873},{"id":9982,"depth":469,"text":9983},{"id":10059,"depth":469,"text":10060},{"id":10303,"depth":469,"text":10304},{"id":10380,"depth":469,"text":10381},{"id":10459,"depth":469,"text":10460,"children":13282},[13283,13284,13286],{"id":10527,"depth":1158,"text":10528},{"id":10668,"depth":1158,"text":13285},"Using sort_union(...) 是什么意思",{"id":10780,"depth":1158,"text":10781},{"id":10835,"depth":469,"text":10836},{"id":10888,"depth":469,"text":10889},{"id":10955,"depth":469,"text":10956,"children":13290},[13291,13292,13293],{"id":11054,"depth":1158,"text":11054},{"id":11211,"depth":1158,"text":11211},{"id":11291,"depth":1158,"text":11291},{"id":11403,"depth":469,"text":11404},{"id":11445,"depth":469,"text":11446,"children":13296},[13297,13299,13301,13303,13305,13307,13309,13310,13312,13314,13315,13316],{"id":11570,"depth":1158,"text":13298},"1. type 到底表示什么",{"id":11647,"depth":1158,"text":13300},"2. system：表中只有一行",{"id":11678,"depth":1158,"text":13302},"3. const：通过主键或唯一键最多定位一行",{"id":11828,"depth":1158,"text":13304},"4. eq_ref：JOIN 时每条前表记录最多匹配一行",{"id":11971,"depth":1158,"text":13306},"5. ref：通过非唯一索引等值查找一组记录",{"id":12090,"depth":1158,"text":13308},"6. range：扫描索引中的一个或多个区间",{"id":12283,"depth":1158,"text":12284},{"id":12399,"depth":1158,"text":13311},"8. index：把整棵索引扫描一遍",{"id":12527,"depth":1158,"text":13313},"9. ALL：扫描整张表",{"id":12631,"depth":1158,"text":12632},{"id":12785,"depth":1158,"text":12786},{"id":13006,"depth":1158,"text":13317},"12. 不要只根据 type 判断性能",{"id":13091,"depth":469,"text":13092},{"id":13145,"depth":469,"text":13146,"children":13320},[13321,13322,13323,13325,13327,13328],{"id":13149,"depth":1158,"text":13150},{"id":13156,"depth":1158,"text":13157},{"id":13163,"depth":1158,"text":13324},"3. !=、NOT IN、IS NULL 一定不走索引",{"id":13176,"depth":1158,"text":13326},"4. key 不为 NULL 就说明索引使用良好",{"id":13185,"depth":1158,"text":13186},{"id":13192,"depth":1158,"text":13329},"6. 使用 FORCE INDEX 就能解决问题",{"id":13202,"depth":469,"text":13203},"wiki:java:mysql-index-failure-scenarios","从联合索引、函数运算、隐式转换、LIKE、OR、选择性和优化器成本等角度，系统解释 MySQL 索引无法使用、部分使用及主动放弃索引的常见场景。",{},77,"\u002Fjava\u002Fmysql-index-failure-scenarios",{"title":9596,"description":13332},"java\u002Fmysql-index-failure-scenarios","AqnNs3qUNTn8ktXgQNQWmCN5cMkFaGtftEusXnNhnmY",{"id":4,"title":5,"body":13340,"commentId":1205,"description":1206,"difficulty":1207,"draft":1208,"extension":1209,"meta":14163,"navigation":1211,"order":1212,"path":1213,"section":1214,"seo":14164,"stem":1216,"updated":1217,"__hash__":1218},{"type":7,"value":13341,"toc":14108},[13342,13344,13346,13348,13366,13368,13370,13376,13378,13385,13387,13389,13391,13445,13447,13449,13459,13461,13463,13465,13467,13472,13474,13476,13480,13482,13484,13500,13502,13507,13509,13511,13513,13521,13523,13525,13527,13529,13531,13536,13538,13543,13549,13551,13553,13555,13560,13562,13564,13566,13568,13570,13572,13574,13576,13578,13583,13585,13599,13601,13603,13605,13617,13619,13621,13623,13625,13630,13632,13644,13646,13651,13653,13655,13671,13673,13678,13680,13682,13686,13698,13700,13702,13704,13709,13713,13715,13717,13719,13721,13725,13727,13733,13735,13740,13742,13747,13749,13754,13756,13758,13770,13772,13774,13776,13781,13783,13795,13797,13799,13801,13806,13808,13854,13856,13858,13860,13862,13867,13869,13879,13881,13883,13885,13890,13894,13902,13904,13906,13908,13910,13922,13924,13926,13931,13941,13943,13945,13950,13952,13954,13956,13961,13963,13989,13991,13993,14009,14011,14021,14023,14025,14027,14029,14031,14033,14035,14037,14039,14041,14043,14045,14047,14049,14051,14053,14055,14057,14059,14061,14063,14065,14067,14069,14106],[10,13343,5],{"id":12},[14,13345,16],{"id":16},[18,13347,20],{},[22,13349,13350,13354,13358,13362],{},[25,13351,13352,31],{},[28,13353,30],{},[25,13355,13356,37],{},[28,13357,36],{},[25,13359,13360,43],{},[28,13361,42],{},[25,13363,13364,49],{},[28,13365,48],{},[18,13367,52],{},[18,13369,55],{},[57,13371,13372],{},[18,13373,13374],{},[28,13375,63],{},[14,13377,66],{"id":66},[18,13379,13380],{},[70,13381,13383],{"href":72,"target":73,"rel":13382},[75,76],[78,13384],{"src":72,"alt":80},[14,13386,83],{"id":83},[14,13388,87],{"id":86},[18,13390,90],{},[92,13392,13393,13403],{},[95,13394,13395],{},[98,13396,13397,13399,13401],{},[101,13398,103],{},[101,13400,106],{},[101,13402,109],{},[111,13404,13405,13415,13425,13435],{},[98,13406,13407,13411,13413],{},[116,13408,13409],{},[119,13410,121],{},[116,13412,124],{},[116,13414,127],{},[98,13416,13417,13421,13423],{},[116,13418,13419],{},[119,13420,134],{},[116,13422,137],{},[116,13424,140],{},[98,13426,13427,13431,13433],{},[116,13428,13429],{},[119,13430,147],{},[116,13432,150],{},[116,13434,153],{},[98,13436,13437,13441,13443],{},[116,13438,13439],{},[119,13440,160],{},[116,13442,163],{},[116,13444,166],{},[18,13446,169],{},[18,13448,172],{},[174,13450,13451,13453,13455,13457],{},[25,13452,178],{},[25,13454,181],{},[25,13456,184],{},[25,13458,187],{},[18,13460,190],{},[14,13462,194],{"id":193},[196,13464,199],{"id":198},[18,13466,202],{},[204,13468,13470],{"className":13469,"code":208,"language":209,"meta":210},[207],[119,13471,208],{"__ignoreMap":210},[18,13473,215],{},[18,13475,218],{},[57,13477,13478],{},[18,13479,223],{},[196,13481,227],{"id":226},[18,13483,230],{},[174,13485,13486,13488,13490,13492,13494,13496,13498],{},[25,13487,235],{},[25,13489,238],{},[25,13491,241],{},[25,13493,244],{},[25,13495,247],{},[25,13497,250],{},[25,13499,253],{},[18,13501,256],{},[204,13503,13505],{"className":13504,"code":260,"language":209,"meta":210},[207],[119,13506,260],{"__ignoreMap":210},[18,13508,265],{},[196,13510,269],{"id":268},[18,13512,272],{},[174,13514,13515,13517,13519],{},[25,13516,277],{},[25,13518,280],{},[25,13520,283],{},[18,13522,286],{},[14,13524,290],{"id":289},[196,13526,294],{"id":293},[18,13528,297],{},[18,13530,300],{},[204,13532,13534],{"className":13533,"code":304,"language":209,"meta":210},[207],[119,13535,304],{"__ignoreMap":210},[18,13537,309],{},[204,13539,13541],{"className":13540,"code":313,"language":209,"meta":210},[207],[119,13542,313],{"__ignoreMap":210},[18,13544,318,13545,322,13547,326],{},[28,13546,321],{},[28,13548,325],{},[196,13550,330],{"id":329},[18,13552,333],{},[18,13554,336],{},[204,13556,13558],{"className":13557,"code":340,"language":209,"meta":210},[207],[119,13559,340],{"__ignoreMap":210},[18,13561,345],{},[196,13563,349],{"id":348},[18,13565,352],{},[354,13567,356],{"id":356},[18,13569,359],{},[354,13571,362],{"id":362},[18,13573,365],{},[354,13575,368],{"id":368},[18,13577,371],{},[204,13579,13581],{"className":13580,"code":375,"language":209,"meta":210},[207],[119,13582,375],{"__ignoreMap":210},[196,13584,381],{"id":380},[174,13586,13587,13589,13591,13593,13595,13597],{},[25,13588,386],{},[25,13590,389],{},[25,13592,392],{},[25,13594,395],{},[25,13596,398],{},[25,13598,401],{},[196,13600,405],{"id":404},[18,13602,408],{},[18,13604,411],{},[174,13606,13607,13609,13611,13613,13615],{},[25,13608,416],{},[25,13610,419],{},[25,13612,422],{},[25,13614,425],{},[25,13616,428],{},[18,13618,431],{},[14,13620,435],{"id":434},[196,13622,439],{"id":438},[18,13624,442],{},[204,13626,13628],{"className":13627,"code":446,"language":209,"meta":210},[207],[119,13629,446],{"__ignoreMap":210},[18,13631,451],{},[204,13633,13634],{"className":454,"code":455,"language":456,"meta":210,"style":210},[119,13635,13636,13640],{"__ignoreMap":210},[460,13637,13638],{"class":462,"line":463},[460,13639,466],{},[460,13641,13642],{"class":462,"line":469},[460,13643,472],{},[196,13645,476],{"id":475},[204,13647,13649],{"className":13648,"code":480,"language":209,"meta":210},[207],[119,13650,480],{"__ignoreMap":210},[18,13652,485],{},[196,13654,489],{"id":488},[174,13656,13657,13659,13661,13663,13665,13667,13669],{},[25,13658,494],{},[25,13660,497],{},[25,13662,500],{},[25,13664,503],{},[25,13666,506],{},[25,13668,509],{},[25,13670,512],{},[18,13672,515],{},[204,13674,13676],{"className":13675,"code":519,"language":209,"meta":210},[207],[119,13677,519],{"__ignoreMap":210},[18,13679,524],{},[196,13681,528],{"id":527},[18,13683,531,13684,535],{},[28,13685,534],{},[174,13687,13688,13690,13692,13694,13696],{},[25,13689,540],{},[25,13691,543],{},[25,13693,546],{},[25,13695,549],{},[25,13697,552],{},[18,13699,555],{},[196,13701,559],{"id":558},[18,13703,562],{},[204,13705,13707],{"className":13706,"code":566,"language":209,"meta":210},[207],[119,13708,566],{"__ignoreMap":210},[18,13710,571,13711,575],{},[119,13712,574],{},[18,13714,578],{},[14,13716,582],{"id":581},[196,13718,586],{"id":585},[18,13720,589],{},[57,13722,13723],{},[18,13724,594],{},[18,13726,597],{},[22,13728,13729,13731],{},[25,13730,602],{},[25,13732,605],{},[18,13734,608],{},[204,13736,13738],{"className":13737,"code":612,"language":209,"meta":210},[207],[119,13739,612],{"__ignoreMap":210},[196,13741,618],{"id":617},[204,13743,13745],{"className":13744,"code":622,"language":209,"meta":210},[207],[119,13746,622],{"__ignoreMap":210},[18,13748,627],{},[204,13750,13752],{"className":13751,"code":631,"language":209,"meta":210},[207],[119,13753,631],{"__ignoreMap":210},[18,13755,636],{},[196,13757,489],{"id":639},[174,13759,13760,13762,13764,13766,13768],{},[25,13761,644],{},[25,13763,647],{},[25,13765,650],{},[25,13767,653],{},[25,13769,656],{},[196,13771,660],{"id":659},[18,13773,663],{},[18,13775,666],{},[204,13777,13779],{"className":13778,"code":670,"language":209,"meta":210},[207],[119,13780,670],{"__ignoreMap":210},[18,13782,675],{},[174,13784,13785,13787,13789,13791,13793],{},[25,13786,680],{},[25,13788,683],{},[25,13790,686],{},[25,13792,689],{},[25,13794,692],{},[18,13796,695],{},[14,13798,699],{"id":698},[18,13800,702],{},[204,13802,13804],{"className":13803,"code":706,"language":209,"meta":210},[207],[119,13805,706],{"__ignoreMap":210},[18,13807,711],{},[92,13809,13810,13820],{},[95,13811,13812],{},[98,13813,13814,13816,13818],{},[101,13815,720],{},[101,13817,723],{},[101,13819,726],{},[111,13821,13822,13830,13838,13846],{},[98,13823,13824,13826,13828],{},[116,13825,733],{},[116,13827,736],{},[116,13829,739],{},[98,13831,13832,13834,13836],{},[116,13833,744],{},[116,13835,134],{},[116,13837,749],{},[98,13839,13840,13842,13844],{},[116,13841,754],{},[116,13843,757],{},[116,13845,760],{},[98,13847,13848,13850,13852],{},[116,13849,765],{},[116,13851,768],{},[116,13853,771],{},[18,13855,774],{},[14,13857,778],{"id":777},[196,13859,782],{"id":781},[18,13861,785],{},[204,13863,13865],{"className":13864,"code":789,"language":209,"meta":210},[207],[119,13866,789],{"__ignoreMap":210},[18,13868,794],{},[174,13870,13871,13873,13875,13877],{},[25,13872,799],{},[25,13874,802],{},[25,13876,805],{},[25,13878,808],{},[196,13880,812],{"id":811},[18,13882,815],{},[18,13884,818],{},[204,13886,13888],{"className":13887,"code":822,"language":209,"meta":210},[207],[119,13889,822],{"__ignoreMap":210},[18,13891,13892,830],{},[119,13893,829],{},[18,13895,13896,836,13898,839,13900,843],{},[119,13897,835],{},[119,13899,835],{},[119,13901,842],{},[18,13903,846],{},[196,13905,850],{"id":849},[18,13907,853],{},[18,13909,856],{},[174,13911,13912,13914,13916,13918,13920],{},[25,13913,861],{},[25,13915,864],{},[25,13917,867],{},[25,13919,870],{},[25,13921,873],{},[196,13923,877],{"id":876},[18,13925,880],{},[204,13927,13929],{"className":13928,"code":884,"language":209,"meta":210},[207],[119,13930,884],{"__ignoreMap":210},[18,13932,889,13933,892,13935,892,13937,892,13939,899],{},[119,13934,121],{},[119,13936,134],{},[119,13938,147],{},[119,13940,160],{},[196,13942,903],{"id":902},[18,13944,906],{},[204,13946,13948],{"className":13947,"code":910,"language":209,"meta":210},[207],[119,13949,910],{"__ignoreMap":210},[18,13951,915],{},[14,13953,919],{"id":918},[18,13955,922],{},[204,13957,13959],{"className":13958,"code":926,"language":209,"meta":210},[207],[119,13960,926],{"__ignoreMap":210},[18,13962,931],{},[174,13964,13965,13969,13973,13977],{},[25,13966,13967,939],{},[119,13968,938],{},[25,13970,13971,945],{},[119,13972,944],{},[25,13974,13975,951],{},[119,13976,950],{},[25,13978,13979,957,13981,892,13983,892,13985,964,13987,326],{},[119,13980,956],{},[119,13982,121],{},[119,13984,134],{},[119,13986,147],{},[119,13988,160],{},[18,13990,969],{},[18,13992,972],{},[174,13994,13995,14001,14007],{},[25,13996,13997,979,13999,982],{},[119,13998,134],{},[119,14000,121],{},[25,14002,14003,979,14005,982],{},[119,14004,160],{},[119,14006,147],{},[25,14008,991],{},[18,14010,994],{},[174,14012,14013,14015,14017,14019],{},[25,14014,999],{},[25,14016,1002],{},[25,14018,1005],{},[25,14020,1008],{},[18,14022,1011],{},[14,14024,1015],{"id":1014},[196,14026,1019],{"id":1018},[18,14028,1022],{},[196,14030,1026],{"id":1025},[18,14032,1029],{},[196,14034,1033],{"id":1032},[18,14036,1036],{},[196,14038,1040],{"id":1039},[18,14040,1043],{},[196,14042,1047],{"id":1046},[18,14044,1050],{},[14,14046,1054],{"id":1053},[196,14048,1058],{"id":1057},[18,14050,1061],{},[196,14052,1065],{"id":1064},[18,14054,1068],{},[196,14056,1072],{"id":1071},[18,14058,1075],{},[196,14060,1079],{"id":1078},[18,14062,1082],{},[196,14064,1086],{"id":1085},[18,14066,1089],{},[14,14068,1092],{"id":1092},[174,14070,14071,14076,14081,14086,14091,14096,14101],{},[25,14072,14073],{},[70,14074,1102],{"href":1099,"rel":14075},[1101],[25,14077,14078],{},[70,14079,1109],{"href":1107,"rel":14080},[1101],[25,14082,14083],{},[70,14084,1116],{"href":1114,"rel":14085},[1101],[25,14087,14088],{},[70,14089,1123],{"href":1121,"rel":14090},[1101],[25,14092,14093],{},[70,14094,1130],{"href":1128,"rel":14095},[1101],[25,14097,14098],{},[70,14099,1137],{"href":1135,"rel":14100},[1101],[25,14102,14103],{},[70,14104,1144],{"href":1142,"rel":14105},[1101],[1146,14107,1148],{},{"title":210,"searchDepth":469,"depth":469,"links":14109},[14110,14111,14112,14113,14114,14119,14126,14133,14139,14140,14147,14148,14155,14162],{"id":16,"depth":469,"text":16},{"id":66,"depth":469,"text":66},{"id":83,"depth":469,"text":83},{"id":86,"depth":469,"text":87},{"id":193,"depth":469,"text":194,"children":14115},[14116,14117,14118],{"id":198,"depth":1158,"text":199},{"id":226,"depth":1158,"text":227},{"id":268,"depth":1158,"text":269},{"id":289,"depth":469,"text":290,"children":14120},[14121,14122,14123,14124,14125],{"id":293,"depth":1158,"text":294},{"id":329,"depth":1158,"text":330},{"id":348,"depth":1158,"text":349},{"id":380,"depth":1158,"text":381},{"id":404,"depth":1158,"text":405},{"id":434,"depth":469,"text":435,"children":14127},[14128,14129,14130,14131,14132],{"id":438,"depth":1158,"text":439},{"id":475,"depth":1158,"text":476},{"id":488,"depth":1158,"text":489},{"id":527,"depth":1158,"text":528},{"id":558,"depth":1158,"text":559},{"id":581,"depth":469,"text":582,"children":14134},[14135,14136,14137,14138],{"id":585,"depth":1158,"text":586},{"id":617,"depth":1158,"text":618},{"id":639,"depth":1158,"text":489},{"id":659,"depth":1158,"text":660},{"id":698,"depth":469,"text":699},{"id":777,"depth":469,"text":778,"children":14141},[14142,14143,14144,14145,14146],{"id":781,"depth":1158,"text":782},{"id":811,"depth":1158,"text":812},{"id":849,"depth":1158,"text":850},{"id":876,"depth":1158,"text":877},{"id":902,"depth":1158,"text":903},{"id":918,"depth":469,"text":919},{"id":1014,"depth":469,"text":1015,"children":14149},[14150,14151,14152,14153,14154],{"id":1018,"depth":1158,"text":1019},{"id":1025,"depth":1158,"text":1026},{"id":1032,"depth":1158,"text":1033},{"id":1039,"depth":1158,"text":1040},{"id":1046,"depth":1158,"text":1047},{"id":1053,"depth":469,"text":1054,"children":14156},[14157,14158,14159,14160,14161],{"id":1057,"depth":1158,"text":1058},{"id":1064,"depth":1158,"text":1065},{"id":1071,"depth":1158,"text":1072},{"id":1078,"depth":1158,"text":1079},{"id":1085,"depth":1158,"text":1086},{"id":1092,"depth":469,"text":1092},{},{"title":5,"description":1206},1785031317584]