4.1 热点数据:识别与打散

分库分表解决了总量问题,但解决不了「倾斜」问题。如果某个分片装的全是大商家的订单,这个分片会比其他分片负载高 5 倍,成为木桶最短的那块板。这一节讲我们如何识别热点、如何用「打散」手段把热点摊平。

场景:少数 Key 占了多数流量

热点数据的典型形态是:少量 Key 承担了大部分访问。订单系统里最明显的热点有两个。一个是「大店铺」:某头部商家日均 3 万单,占全站单量的 4%,它的订单全集中在少数几个分片。另一个是「热门商品」:秒杀类商品的库存行是瞬时读写焦点,QPS 可以冲到 8000。

热点类型 典型对象 访问特征 危害
写热点 秒杀库存行 单行 QPS 8000 行锁排队、连接耗尽
读热点 爆款商品详情 单 Key QPS 2 万 缓存击穿、IO 打满
分片热点 大店铺订单写 单片写 QPS 600 分片倾斜
时间热点 大促前 10 分钟 全片瞬时高峰 整体抖动

识别热点的手段是埋点加采样:在数据访问层记录每个 Key 的访问计数,写入 CLS 统一归并,按分钟粒度出 TOP 热点榜。这套采样每天带来约 2% 的性能损耗,但换来的是「热点可观测」——4.3 节的慢查询定位也依赖同一套归集链路。

方案:三层打散策略

热点的处理按「离用户越近越好」的分层原则:读热点先在应用层加本地缓存,再在 Redis 层分片,最后才到数据库;写热点则只能靠「拆 Key」把单行压力拆成多行。单行拆多行是库存场景的经典操作:库存 1000 拆成 10 个子库存行各 100,扣减时随机选一行轮询扣,单行锁竞争从 8000 QPS 降到 800 QPS。

打散手段 适用热点 实现成本 局限
应用本地缓存 读热点 低 一致性靠 TTL
Redis 分片缓存 读热点 低 缓存穿透风险
库存子行拆分 写热点 中 扣减逻辑变复杂
订单二次哈希 分片热点 中 改路由规则
限流与排队 时间热点 低 牺牲吞吐

秒杀类库存拆行的完整设计可以参考 PHP 实战:秒杀系统,那本书对此有更细致的代码级讨论,核心理念都是把单点竞争摊平。

热点监控大盘

热点治理的前提是热点可见。我们的监控大盘分三层:分钟级 Key 计数榜、分片级负载对比、接口级耗时分布。三层各有用途:Key 榜用来发现热点本身,分片对比用来发现倾斜,耗时分布用来评估热点影响面。

监控维度 数据来源 告警规则 典型问题
Key 访问 TOP 应用层采样 超中位数 10 倍 缓存击穿风险
分片 QPS 对比 数据库监控 最大片超过均值 2 倍 倾斜
分片 IO 对比 云监控 单片超过均值 3 倍 热点已入盘
接口耗时分布 全链路追踪 p99 突增 2 倍 用户已受影响

大盘上线后最有趣的发现是:分片 0 的负载常年比其他片高 30%,因为 OrderID 末位是 0 的订单在某些渠道批次中偏多(哈希前的原始分布有周期性规律)。这再次印证了 2.2 节的经验:灌数测试不能只测均匀数据,要测业务特征子集。

打散方案的组合使用

真实场景里热点不会只有一种形态,打散手段往往组合使用。大促期间我们对头部商家同时用了四层手段,各管一段:

防线 手段 覆盖的问题
第一层 应用本地缓存 5 秒 读热点不落 Redis
第二层 Redis 分片缓存 本地过期后的兜底
第三层 库存子行 10 段 写热点不锁单行
第四层 限流队列 超出容量的流量排队

四层防线的触发顺序固定:请求先撞本地缓存,穿透后走 Redis,写入时打在子行上,流量洪峰超过第四层容量就在队列里排队等待。这套组合把大促期间分片的 QPS 峰值从预估的 9000 压到实际的 2200,剩余压力都在实例舒适区内。容量问题永远比优化问题好解决,打散的终点是限流。

配置:库存子行拆分

库存表从一行库存改为 N 个子库存行,扣减逻辑相应改造。核心是扣减要带子行选择策略,以及库存不足时的「借还」兜底:

CREATE TABLE t_stock_segment (
  sku_id      BIGINT,
  seg_no      INT,
  stock       INT,
  PRIMARY KEY (sku_id, seg_no)
);

扣减时按 seg_no = hash(thread_id) % 10 选行,单行扣不出来就借相邻段的库存。借还操作用一个负债表记录,异步平账。这套逻辑上线后,秒杀时段的库存扣减耗时 p99 从 1200 毫秒降到 80 毫秒。

子行拆分的数量有讲究。拆 10 段把单行竞争降到 800 QPS,拆 100 段降到 80 QPS,但段数越多「借还」越频繁(某段扣完了要去借),管理成本上升。实测 10 到 20 段是甜区。另外子行拆分对「库存正好是单数」的场景有特殊处理要求:末段做零数收容,避免出现库存被拆碎后无法整单扣减的边界 case。

踩坑:热点是动态的

最大的坑是「热点榜单不会自己更新」。系统上线时配的监控阈值是按当时的流量定的,半年后业务增长阈值过时,某天的猝发热点没触发告警。之后我们把热点检测改成「相对阈值」:以过去 7 天同时段中位数为准,超出 3 倍即告警,而不是固定 QPS 数。

第二个坑是缓存击穿。秒杀开始瞬间,热 Key 的本地缓存同时过期,所有请求穿透到数据库。解法是本地缓存的 TTL 加随机抖动(避免同时过期),以及热点 Key 在快过期时主动异步续期。

第三个坑是分片热点打散会破坏按订单号的路由。订单二次哈希(大店铺订单分散到更多分片)让按订单号查单的路由变复杂,需要多查一次映射。最后我们只对一个超级商家做了特殊处理(手动切分到固定 4 个分片),没有做通用的二次哈希——通盘考虑路由复杂度后,少数特殊 case 手工处理更经济,过度设计通用方案往往得不偿失。热点治理的本质是动态再平衡,不存在一劳永逸的方案。

第四个坑是热点治理的「钟摆效应」。某次把秒杀库存拆成 100 段,单行竞争确实降了,但「汇总查询」变慢了:看某 SKU 的总库存要聚合 100 行。拆得越碎,点查越快聚合越慢。最终方案是「拆 10 段加汇总缓存」,聚合结果放 Redis 定时刷新。任何打散都要同时回答「打散后怎么聚合」这个问题。

第五个坑是大促前的「热点预检」。大促开始前两小时,我们会把历史大促的热点店铺清单拉出来预热:提前把这些店铺的缓存与路由信息刷好、库存子行预扣额分配均匀。预检清单是逐次大促积累出来的,第一次搞大促时没有预检,开场 5 分钟缓存击穿直接把某分片打挂了 30 秒。热点治理在平时是优化,在大促前是生存线。

小结

热点治理三层走:读热点上缓存、写热点拆 Key、分片热点做二次哈希或人工切分。监控要用相对阈值跟随业务增长。所有打散手段都有代价(路由变复杂、一致性窗口变长),动手前先确认热点是否真实存在,早期宁可手工处理特殊 case 也不要过度设计通用方案。热点治理的度量指标要写进每周的运维周报,倾斜率与缓存命中率连续三周恶化才触发下一轮治理,避免运动式优化,这一点在治理类工作里比技术本身更关键。

操作清单:热点治理五步

步骤 动作 主要产出
1 埋点 数据访问层加 Key 计数 采样日志
2 归并 CLS 聚合 TOP 榜 热点报表
3 判定 相对阈值告警 告警规则
4 打散 缓存、拆行、切分 代码改造
5 复核 两周后回看倾斜指标 治理报告

五步里最容易被跳过的是第 5 步。打散做完没有复核,就不知道是真治好了还是把热点挪到了别的地方——我们历史上就有一次把分片热点打散后,热点转移到了归并层,总耗时反而上升。复盘制度治好了这种「自以为修好了」。

延伸阅读