Java

热点表、热点索引、热点行,该先拆哪一层?

热点集中不等于立刻分库分表。真正难的是先判断热点到底落在表、索引还是行,再决定先拆写入路径、先打散索引热点,还是先改业务模型。先把等待压在哪一层看清楚,后面的拆分才不会越做越重。

  • MySQL
  • 热点表
  • 热点索引
  • 热点行
  • 架构设计
18 分钟阅读

线上一旦出现下面这些现象,团队很快就会说出同一句话:

  • 某张表总是最忙
  • 某类更新一到高峰就排队
  • 锁等待集中在同一批数据
  • 数据库实例整体没完全打满,但 RT 和连接池已经很难受

那就是:是不是该拆了?

问题在于,很多团队一说“拆”,默认想的就是:

  • 分库分表
  • 按用户拆
  • 按业务线拆
  • 把大表切小

这些动作当然有可能是对的,但热点问题里最容易犯的错,就是还没搞清楚热点到底落在哪一层,就先做最重的拆分。

因为真实项目里的热点,至少可能落在三层完全不同的地方:

  • 热点表:整张表整体太热,读写都集中
  • 热点索引:表不算夸张,但某个索引前缀、某类写入模式、某个 B+ 树区间特别热
  • 热点行:问题更集中,所有事务都在争同一小批记录

这三种热点,处理动作完全不一样。

如果判断错层级,最常见的结果就是:

  • 分了表,热点行还在
  • 分了库,索引写热点还在
  • 拆了业务,锁等待还是没掉
  • 最后复杂度大了,问题却只缓了一点点

所以这篇文章想解决的不是“分库分表怎么做”,而是一个更前置、更值钱的问题:

当你已经确认数据库热点明显存在时,到底先判断它是热点表、热点索引还是热点行?又该先拆哪一层,才最有可能真正把等待链打散?

如果热点已经明显了

你现在看到的现象更适合先看哪条线为什么
热点已经很明显,但团队在争论先拆表、拆索引还是拆业务对象继续看本文这里先帮你把等待究竟压在表、索引还是行上分开
你还没确认是不是锁等待、长事务或普通写放大先看锁等待 / 长事务 / 写放大文章先定位等待落点
你已经确定是少数热 key、Redis 热点先看 Redis 热点治理相关文章不要把缓存热点和数据库热点混在一起
你想要的是分库分表实施细节这篇只能回答“先拆哪层”,不替代实施方案它更偏决策边界

这篇主要帮你先分哪一层

如果你已经知道问题不是普通的 SQL 抖动,而是热点长期压在同一段数据库路径上,这篇就适合继续往下看。

它想回答的是:当等待已经明显集中,但你还没分清热点到底落在 表、索引还是行 上时,下一步该先拆哪一层,才不至于一上来就把分库分表这种最重、最难回退的动作端出来。

先做一轮分层判断

  1. 先问热点是不是高度绑定少数业务对象,如果是,优先怀疑热点行。
  2. 再看热点是不是主要绑定某种写入模式、索引区间或时间窗口,如果是,更像热点索引。
  3. 只有当前两层都解释不住,才更像热点表级压力。
  4. 优先拆最具体的等待点,而不是默认做最大的结构改造。
  5. 再决定先改模型、先打散索引,还是最后才做更大的拆表动作。

一、先别急着谈拆分:热点集中本质上是“等待集中”

很多人把热点理解成“某个地方访问量很大”,这没错,但在数据库现场里,更值得盯的是:

热点问题最终会表现成哪种等待在集中。

常见外显现象通常包括:

  • 某类 SQL 耗时飘得很厉害
  • 锁等待、活跃事务、长事务在高峰期抬头
  • 连接池 active 和 pending 上升
  • 接口 RT 高度集中在某几条业务链路上

这些现象共同说明一件事:

  • 问题不只是“有流量”
  • 而是“有流量 + 资源争抢方式高度集中”

而资源争抢到底集中在:

  • 整张表
  • 某棵索引树的局部
  • 某一批记录

决定了你后面该怎么拆。

二、先分清三种热点:热点表、热点索引、热点行到底有什么区别

1. 热点表:整张表都像交通主干道

热点表更像这种现场:

  • 大量读写都集中在同一张表
  • 不同业务路径都要频繁访问它
  • 表上的多个索引、多个 SQL 类型都很活跃
  • 读写冲突、连接池、实例资源整体都被它拖高

常见例子包括:

  • 订单主表
  • 用户账户表
  • 库存汇总表
  • 全局任务状态表

这类问题的信号通常是:

  • 不只是某个 SQL 热
  • 而是整张表上的多个路径都热
  • 你把一条 SQL 优化掉,别的路径还是热

2. 热点索引:热的不是表,而是某个索引区间或写入模式

这类问题很容易被误看成“整张表太大”。

常见形态是:

  • 自增主键或时间递增索引,把写入压在 B+ 树尾部
  • 二级索引前缀分布极不均匀,某个状态、某个租户、某个分区特别热
  • where status = ? order by create_time desc limit n 这类查询反复扫同一段热点区间
  • 某个联合索引设计,让高并发更新总落在一小块页面上

这时你看到的可能是:

  • 整张表数据量很大,但真正冲突只集中在索引局部
  • 读写等待集中在同一类条件组合
  • 分表以后,如果索引模式不变,热点仍会跟着复制过去

3. 热点行:问题已经缩到同一条或同一小批记录

热点行是最常见、也最容易被误拆的一类。

典型场景包括:

  • 同一库存记录被高并发扣减
  • 同一订单状态被反复流转
  • 同一用户余额、额度、券状态被频繁更新
  • 某个全局计数器、单行汇总记录被所有请求共享

这种场景下,最明显的信号通常是:

  • 锁等待集中在同一主键或同一小批业务键值
  • SQL 本身不算复杂,但耗时时快时慢
  • 高峰期尤其容易出现事务竞争
  • 你就算把表拆大拆小,只要这行语义不变,热点还是热点

三、到底该先拆哪一层?默认顺序不是从大到小,而是从“最具体的等待点”往外拆

很多团队天然喜欢先做大动作,例如分表、分库、拆服务,因为感觉“动作大,见效应该也大”。

但热点问题更稳的顺序通常恰好相反:

先确认是不是热点行,再判断是不是热点索引,最后才判断是不是热点表。

原因很简单:

  • 越具体的热点,越有可能用更小的改动把等待打散
  • 越上层的拆分,成本越高,副作用也越大
  • 如果底层热点没变,越大的拆分越容易只是搬家

换句话说,默认排查顺序更像:

  1. 先找有没有单点或小范围热点行
  2. 再看索引结构和写入分布是否天然集中
  3. 最后才判断整张表是否真的需要按业务或容量层面拆开

四、先判断是不是热点行:因为这是最容易“拆错方向”的层

如果热点集中在行级,最值钱的动作通常不是分库分表,而是改业务模型和并发模型。

常见判断信号

  • 锁等待集中在少数主键
  • 某类 update / select for update 在高峰时明显变慢
  • 高并发更新对象数量并不多
  • 业务上存在“大家都要争同一份状态”的设计

这类问题优先该拆什么

1. 先拆共享状态,不先拆表

例如:

  • 把单行汇总改成分桶计数
  • 把全局库存改成分片库存或预扣减池
  • 把单一状态机拆成局部状态与异步汇总

2. 先拆事务边界,不先拆存储边界

很多热点行问题其实是:

  • 事务里夹了外部调用
  • 状态更新持锁太久
  • 批处理与在线请求同时争同一批行

这时缩事务、调更新顺序、隔离批任务,往往比拆表更快见效。

3. 先拆写入节奏,不先拆读路径

如果同一行被持续高频更新,可以先考虑:

  • 合并写
  • 异步削峰
  • 本地聚合后批量落库
  • 事件化而不是同步逐条改写

什么时候热点行才需要进一步升级到更大层级拆分

只有在下面这些情况同时出现时,才值得继续往外看:

  • 热点对象数量已经很多,不再是少数行
  • 业务模型短期改不了
  • 行级竞争只是更大分布问题的一部分

五、再判断是不是热点索引:因为很多“拆表无效”都败在这里

热点索引比热点行更难察觉,因为它看起来像“很多不同数据都在慢”,但实际上慢的是同一个索引区间。

典型现场长什么样

  • 插入量很大,但总卡在某个递增主键或时间索引尾部
  • 某类联合索引前缀太集中,导致大量读写扫同一段范围
  • 某个状态值、租户值、日期值让二级索引局部极热
  • 表分了,但每个分表里还是同样的尾插、同样的前缀集中

这类问题优先该拆什么

1. 先打散索引分布

常见思路包括:

  • 改主键或业务键生成策略,避免单点递增过度集中
  • 调整联合索引顺序,让高并发条件更均匀分布
  • 对热点维度做 hash 前缀、分桶字段或逻辑分段
  • 热门状态、热门租户单独走更适合的路由

2. 先拆访问模式,不先拆整张表

例如:

  • 把高频分页查询改成游标或更窄索引覆盖
  • 把同一状态的大量轮询,改成增量拉取或事件通知
  • 把“总是扫最新一批”的查询,改成预计算结果集

3. 先拆冷热数据和时间窗口

很多索引热点其实集中在:

  • 最近一天
  • 最近一小时
  • 某个活跃状态集

这时按冷热、时间或状态拆分,往往比粗暴分表更直接。

什么时候索引热点会上升成表级热点

如果你发现:

  • 不同索引、不同访问模式都同时很热
  • 表上的资源争抢已经不限于某个局部区间
  • 优化一个索引热点,另一个索引热点又冒出来

那才说明问题可能已经接近表级。

六、最后才判断是不是热点表:因为这往往意味着业务与容量都已经一起压上来了

热点表是最适合做更大拆分的场景,但前提是你要真的确认:

  • 问题不是少数热点行
  • 也不是单一索引模式
  • 而是整张表在读、写、索引、锁、连接占用上都承受了过于集中的业务压力

常见信号

  • 同一张表承载了过多业务职责
  • 多类 SQL、多个索引都持续高热
  • 归档、冷热分层、读写分离都已经做了,仍然整体偏热
  • 连接池、慢接口、实例负载都长期被它牵着走

这类问题优先该拆什么

1. 先做垂直职责拆分

如果一张表既承载:

  • 主状态
  • 明细信息
  • 审计字段
  • 扩展属性
  • 高并发统计

那最先该做的往往是职责拆分,而不是直接水平切。

2. 再做冷热拆分和生命周期拆分

例如:

  • 活跃订单与历史订单分离
  • 在线记录与归档记录分离
  • 高频更新字段与低频查询字段分离

3. 最后再做水平拆分

只有在下面这些条件已经明确时,水平拆分才真正值得:

  • 业务路由键稳定
  • 热点分布可以随路由一起分散
  • 跨分片代价可接受
  • 不是把热点行、热点索引原样复制到多份表里

七、一个更实用的判断框架:先问“热点是否跟业务对象绑定”,再问“热点是否跟访问模式绑定”

以后再遇到“热点该先拆哪层”,我更建议先问两个问题。

问题 1:热点是不是高度绑定少数业务对象

如果答案是“是”,优先怀疑热点行。

比如:

  • 10 个爆款商品
  • 100 个活跃订单
  • 少数余额账户
  • 少数全局配置行

这类问题最值钱的是改状态共享模型,而不是急着分表。

问题 2:热点是不是主要绑定某种查询 / 写入模式

如果答案是“是”,优先怀疑热点索引。

比如:

  • 尾插入特别热
  • 某个状态分页总扫最新一段
  • 某个联合索引前缀过于集中

这类问题最值钱的是打散索引和访问模式,而不是复制更多表。

如果上面两个问题都不是,才更像热点表

也就是:

  • 热度既不只集中在少数对象
  • 也不只集中在某一类索引模式
  • 而是整张表整体成为共享瓶颈

这时更大的拆分才有意义。

八、一个典型案例:为什么“分表了还是慢”

假设某个库存扣减系统,之前一张库存表在高峰期锁等待很重。

团队做了第一轮改造:

  • 按商家分表
  • 表数量多了很多
  • 数据量也明显分散了

结果上线后发现:

  • 大部分时段确实好一些
  • 但活动高峰时,核心爆款商品的扣减还是慢
  • 锁等待仍然集中在少量 SKU

这说明什么

这说明原问题的主因不是热点表,而是热点行。

因为:

  • 不管表怎么分
  • 爆款 SKU 还是那几个
  • 同一 SKU 的库存还是被所有请求争

如果不把:

  • 单行库存改成分桶库存
  • 扣减改成异步削峰或预占池
  • 事务边界缩短

那分表只是把背景流量分散了,核心冲突没有被打散。

这种案例特别典型,也最能说明为什么默认顺序应该从“小热点”往外查,而不是从“大动作”往下压。

九、关键误判:这类问题最容易在哪些地方走偏

误判 1:看到表名总出现在慢 SQL 里,就认定是热点表

不一定。

它也可能只是这张表里的少数索引区间或少数行在热。

误判 2:分库分表是通用解法,所以应该优先做

热点行和热点索引如果没被打散,分库分表只是在复制问题。

误判 3:行锁等待重,就说明数据库参数或引擎问题

很多时候根因在业务共享状态设计、事务边界和写入节奏,而不是数据库参数。

误判 4:索引热点就是索引建得不好

有时索引本身没错,错的是访问模式太集中,或者业务键分布天然偏斜。

误判 5:拆表以后指标稍微变好,就说明方向一定对

有时只是背景噪声降低了,但主热点仍在。要继续看锁等待、连接池、P99 和高峰窗口,而不是只看平均值。

十、FAQ:热点表、热点索引、热点行问题里最常被问到的几个问题

1. 默认应该先怀疑哪一层?

默认更建议先排热点行,再排热点索引,最后才排热点表。因为越具体的热点,越可能用更小的改动打散。

2. 热点索引和热点表最容易怎么混淆?

当某张表上的某一类查询、某一种写入模式特别热时,很容易误以为“整张表都该拆”。实际上真正热的可能只是某棵索引树尾部或某个前缀区间。

3. 热点行问题一定只能靠业务改造吗?

大多数真正有效的解法确实在业务模型、事务边界和写入节奏上,而不是单纯数据库参数。参数最多缓一口气,难以根治。

4. 什么时候分库分表才值得提上日程?

当你已经确认:

  • 热点不是少数行
  • 也不是单一索引模式
  • 整张表职责过重、生命周期过长、整体访问持续高热

这时更大的拆分才真正有收益。

十一、现场继续排时,我通常会顺手拉上这几篇

这篇先把“热点行 -> 热点索引 -> 热点表”的判断顺序摆清。真到线上继续追,我一般会顺着已经露出来的等待信号往下看,而不是先把最大的结构动作端出来。

写这篇时,我最怕的就是团队还没坐实等待落点,就先把分库分表这种大动作端上来。先把等待到底压在行、索引还是整张表看准,后面的模型改造、索引打散、分库分表才不至于越做越重。

十二、最后想强调的其实只有一件事

热点问题最容易把人带偏的地方,是一看到“集中”就想立刻做最大的拆分。

我更认同的顺序是:

先找最具体的等待点,先判断是不是少数行在争、是不是索引局部在热,最后才判断整张表是否真的需要更大层级的拆分。

只要这条顺序不乱,很多原本会走向“复杂度先上、收益却不稳”的热点治理,最后都会收敛成一个更具体、也更能落地的拆分决策。