一张订单表从百万行涨到几亿行之后,加索引、优化 SQL 的收益会越来越小——因为 95% 的查询只碰最近三个月的数据,却要为三年前的历史买单。数据冷热分离就是针对这个结构性问题:把不同温度的数据放到不同成本的存储里。本文讲清冷热怎么判、架构怎么搭、迁移怎么保证一致、查询怎么路由。

冷热数据的判定维度

热数据是当前被高频读写、要求毫秒级响应的数据;冷数据是访问稀少但还不能删的数据。判定冷热有三个常用维度,实践中经常组合使用:

  1. 时间维度:按创建时间或最后更新时间划线,例如 90 天前的订单转为冷数据。实现最简单,适合订单、日志、账单这类访问频率与时间强相关的业务。
  2. 访问频率维度:统计行级访问次数,低于阈值则判冷。适合内容、商品这类”老数据也可能突然爆火”的场景,但需要额外的访问统计机制,成本较高。
  3. 业务状态维度:按业务终态划分,例如”已完成且过了售后期”的订单才允许转冷,进行中的工单永远是热数据。这条维度常被忽略,却最能避免把还在被业务纠缠的数据误迁走。

判定规则不该由技术团队闭门决定,客服查单、财务对账、审计调阅都会影响”冷数据还能不能查、多久能查出来”。落地前先把所有查询方找齐,再定冷却线。

分离架构:热库与冷库

实际落地很少只有冷热两档,而是渐进分层:

层级 数据示例 存储策略
热层 近 90 天 + 未完成订单 MySQL 主库(SSD),承载全部在线读写
温层 90 天至 1 年 MySQL 归档实例(廉价盘),可在线查询但性能要求低
冷层 1 至 3 年 ClickHouse/Doris 等 OLAP 引擎,或 MySQL 归档表
归档层 3 年以上 对象存储,仅存明细或汇总,极少读取

冷库选型按查询需求定:需要事务和复杂条件查询就用 MySQL 冷库,运维成本最低;以统计分析和报表为主就选 ClickHouse 这类列存引擎,压缩率高、聚合快;几乎只读的超大规模明细可以直接落对象存储。热库瘦身后的收益是立竿见影的:索引变浅、Buffer Pool 命中率回升、DDL 和备份耗时大幅下降。

数据迁移与一致性

迁移的主流做法是定时任务分批搬迁,关键要求是可重入、可回滚、不锁业务:

  1. 复制:按主键范围分批读出满足冷却条件的数据,INSERT IGNORE 写入冷库,保证重复执行不产生脏数据。
  2. 校验:对比冷热两侧的行数和关键字段摘要(如订单金额合计),通过才进入下一步。
  3. 切读:查询路由把这部分数据的读流量切到冷库。
  4. 延迟删除:观察一段时间后(几天到几周)再清理热库数据,出问题可以随时切回。

最棘手的是迁移期间数据被更新。推荐”状态标记 + 幂等”方案:热表加 migrate_status 字段,迁移前先把记录从 0 改为 1(带条件的 UPDATE 天然是乐观锁,抢不到就跳过),写入冷库后再改为 2,删除时校验状态为 2。期间业务若更新了这条记录,状态流转条件会失败,该记录本轮跳过、下轮重试。注意冷热是两个实例,INSERT 冷库和 DELETE 热库无法放进一个本地事务,只能靠状态机和重试逼近最终一致。另外,大范围扫表迁移会污染 Buffer Pool,务必按主键范围分批、限制单批大小、放在低峰期跑。

查询路由与冷数据回温

应用层根据查询条件决定去哪个库:按时间范围路由是最常见的方式,规则放进配置中心动态调整。查询横跨冷热两侧时(如”查近两年的订单”),分别查询后由应用层合并;如果还带全局排序分页,偏移量大时应用层归并极易撑爆内存,应该限制查询跨度,或把这类分析流量引到同步了全量数据的 OLAP 宽表上。

冷数据突然被频繁访问(用户翻出了三年前的订单)怎么办?多数场景用”不回迁 + 缓存兜底“:查询先过缓存,未命中再查冷库并把结果写入缓存,给几分钟到十几分钟的短 TTL 即可,没必要为了偶发访问把数据搬回热库。真正的持续高热再考虑异步回迁。为防恶意请求用随机参数打穿冷库,缓存前可以加布隆过滤器或对不存在的数据缓存空值。

权衡与小结

  • 冷热分离的收益是热库瘦身后的全面提速和存储成本下降(SSD 与大容量盘单价差数倍),代价是迁移链路、路由逻辑、一致性校验整套复杂度。
  • 判定维度优先用时间,辅以业务状态,访问频率维度成本最高只在必要时引入。
  • 迁移宁可慢不能错:复制、校验、切读、延迟删除四步走完,每一步都可回滚。
  • 跨冷热查询是体验洼地,要么限制范围,要么交给 OLAP 引擎,别在应用层做大跨度归并。
  • 数据量没到千万级、查询慢先查索引和 SQL——冷热分离是结构性手段,不该是慢查询的第一反应。