大宗商品销采运储系统・六层架构连载(数据持久层)
上一篇我们通过分布式事务,解决了跨库单据流转的数据一致性问题。
到这里,分库分表做了,读写分离有了,事务也稳了,系统是不是就高枕无忧了?
并没有。还有一头“房间里的大象”在悄悄压垮你的数据库:历史数据。
大宗商品系统运行三年后,单表数据量可能逼近几千万甚至上亿行。其中 80% 是历史单据(已完结的运单、三年前的出库单)。这些数据业务员平时根本不查,但它们在物理上死死占着磁盘空间,甚至污染了 MySQL 的 Buffer Pool(内存缓存)。
如果不做冷热分离,你分再多的表,也会被这些“死数据”拖死。
一句话定调:
冷热分离不是为了省磁盘,而是为了给数据库做“减负手术”。让热数据跑得更快,让冷数据查得到但不碍事。
一、痛点:历史单据,是如何拖垮系统的?
1. 缓存污染(最致命)
InnoDB 的内存是有限的。当业务员查询一张新单据时,MySQL 需要把数据页从磁盘读入内存。如果历史大表占据了大量空间,甚至导致内存频繁换入换出(Swap),热数据的查询延迟会从 10ms 飙升到 500ms。
2. 索引树变深
几千万行甚至上亿行的数据,B+ 树的层级会变深(通常从 3 层变成 4 层),每次查询都要多一次磁盘 IO。
3. 备份与 DDL 灾难
一个几百 GB 的数据库,做一次全量备份要几个小时,恢复起来更是噩梦。如果要给单据表加个字段,直接 ALTER TABLE 动辄锁表几十分钟,业务全线瘫痪。
4. 合规与审计的红线
根据《会计档案管理办法》等合规要求,大宗商品的交易单据、财务凭证往往需要保留 10 年甚至 30 年。
你不能直接 DELETE,只能分离。

二、冷热数据怎么划分?(大宗商品业务标准)
不能拍脑袋决定什么是冷、什么是热,必须结合业务状态和时间维度。
1. 按时间维度(最常用)
-
热数据:近 3 个月的单据。业务员高频查询、修改、流转。
-
温数据:3 个月 ~ 1 年的单据。偶尔查询对账。
-
冷数据:1 年以上的单据。基本只有财务审计、历史凭证核查时才会看。
2. 按状态维度(更精准)
-
热数据:
状态 = 流转中 / 待审核 / 待出库。只要没走完流程,不管放多久,都是热数据。 -
冷数据:
状态 = 已完结 / 已作废 / 已红冲,且完结时间 > 3个月。
判定公式:
冷数据 = (状态 IN ('FINISHED', 'CANCELLED')) AND (完结时间 < NOW() - 3个月)
三、架构演进:冷热数据分级存储方案
业界标准的冷热分离架构,通常分为三层:
应用服务层→ 路由层(ShardingSphere / 业务代码拦截)→ 【热库】MySQL 主集群(分库分表,高配SSD):近3个月数据→ 【温库】MySQL 从库 / 归档库(低配HDD):3个月~1年数据→ 【冷库】对象存储(OSS/S3)或 ClickHouse / ES:1年以上数据
1. 热库:MySQL 分库分表
只保留最近 3 个月的数据。单表数据量保持在 500 万行以内,保证极速响应。
2. 温库:独立的归档 MySQL 实例
将 3 个月到 1 年的数据迁移至此。使用普通的 HDD 磁盘即可。查询频率低,可以接受一定的延迟。
3. 冷库:大数据的归宿
-
如果仅仅是合规要求(存起来不查):直接导出为 CSV / Parquet 文件,存入阿里云 OSS / 腾讯云 COS / 私有云MINIO,成本极低。
-
如果偶尔需要检索(如查三年前的订单):将历史数据同步到 Elasticsearch 或 ClickHouse,提供只读查询接口。

四、落地实战:历史数据平滑归档方案
绝不能写个 DELETE 脚本直接删,那样会导致主从延迟、锁表,甚至误删。

步骤 1:数据归档任务(基于调度服务)
利用我们连载过的“基础服务层 - 调度服务”,每天凌晨低峰期触发归档任务。
核心逻辑:先复制、后删除。
@Scheduled(cron = "0 0 3 * * ?") // 每天凌晨3点执行
public void archiveColdData() {
// 1. 确定要归档的截止时间(3个月前)
LocalDateTime threshold = LocalDateTime.now().minusMonths(3);
// 2. 分批查询符合条件的冷数据(每次查 1000 条,防止 OOM)
while (true) {
List<Order> coldOrders = orderMapper.selectColdData(threshold, 1000);
if (coldOrders.isEmpty()) break;
// 3. 批量写入冷库(归档表 / ClickHouse / 导出文件)
archiveMapper.batchInsert(coldOrders);
// 4. 记录日志,确认落盘成功
log.info("成功归档 {} 条数据", coldOrders.size());
// 5. 物理删除热库数据(按主键删除,防止锁全表)
List<Long> ids = coldOrders.stream().map(Order::getId).collect(Collectors.toList());
orderMapper.batchDeleteByIds(ids);
// 6. 稍微休眠,避免把主库 IO 打满
Thread.sleep(100);
}
}
⚠️ 严禁写
DELETE FROM t_order WHERE create_time < '2023-01-01',这会造成巨大的长事务,主从延迟瞬间飙升,甚至拖垮整个数据库。
步骤 2:应用层冷热路由拦截
数据归档后,当业务员查询一个单号时,系统怎么知道去热库查,还是去冷库查?
方案 A:代码层判断(推荐)
先查热库,查不到再查冷库。
public OrderDTO getOrderByNo(String orderNo) {
// 1. 查热库(基因法路由)
OrderDTO order = orderMapper.selectByOrderNo(orderNo);
if (order != null) return order;
// 2. 热库没有,查冷库
return archiveMapper.selectByOrderNo(orderNo);
}
方案 B:基于时间的路由拦截器
在 ShardingSphere 中配置,如果查询条件包含 create_time,且小于 3 个月,强制路由到归档数据源。
五、冷热分离的踩坑与红线
坑点 1:主从延迟急剧升高
如果归档任务执行得很快,MySQL 产生大量 binlog,从库回放跟不上,导致线上读请求读到了“正在被删除的数据”,引发幻觉。
规范:归档任务必须分批(Batch),并在每批之间 sleep 几十毫秒,限制 TPS。
坑点 2:冷数据查询超时
如果冷库是 ClickHouse 或 OSS,查询延迟必然比 MySQL 高。
规范:冷数据的查询接口,必须加超时控制(如 3 秒),且前端展示要增加 Loading 提示。同时,冷库查询要加 Sentinel 限流。
坑点 3:跨库查询与统计
一旦部分数据进了冷库,原来 SELECT COUNT(*) FROM t_order 就不准了。
规范:统计报表必须走数仓(离线 T+1)。绝对禁止在业务库里做跨热冷库的实时聚合。
六、本篇小结
数据是有生命周期的。让热数据留在最贵、最快的介质上,让冷数据流向廉价、大容量的存储中,是数据库架构设计的必然选择。
记住三句话:
-
冷热划分看状态和时间,已完结的尽早挪走。
-
归档要分批软删除,严禁长事务大 Delete。
-
热库查不到去冷库查,统计报表走数仓。
下篇预告:数据量的问题解决了,但如果你的 SQL 写得烂(比如没走索引、用了 SELECT *),再好的架构也救不了你。下一篇,我们讲述:慢 SQL 治理与数据库性能压舱石。拆解如何通过监控、Explain 分析、索引优化、连接池调优,守住数据库的最后一道防线
本文聚焦微服务架构中历史单据导致的数据库性能瓶颈,系统阐述冷热数据划分标准(时间+状态维度)、三级分级存储方案(热库MySQL分库分表、温库归档MySQL、冷库OSS/ClickHouse/ES),并详解平滑归档任务调度与应用层路由拦截机制,同时指出主从延迟、冷查询超时、跨库统计等关键落地风险及规避规范。
402

被折叠的 条评论
为什么被折叠?



