项目经验_之_冷热分离实战
前言
Github:https://github.com/HealerJean
来自字节:https://mp.weixin.qq.com/s/ZKRkZP6rLHuTE1wvnqmAPQ
一、冷热分离场景
1. 背景
某电商平台每日生成约 4000万条保单记录。每笔客户订单在其生命周期中可能触发多个保单创建事件(如:下单时创建商品保险、发货时创建运输险、保修期开始时创建延保服务等)。因此,一个订单号可关联多个保单,且这些保单的创建时间可能横跨数月甚至一年以上。
2. 现状
1)客户保单表:
- 分片键:客户 ID(所有查询必须包含客户 ID)
- 唯一索引:保单号(
policy_no) - 支持查询:
- 客户 ID + 订单号 → 查询该客户下某订单的所有关联保单
- 客户 ID + 保单号 → 精确查询单条保单
2)供应商保单表,出现了数据倾斜的问题:
- 分片键:供应商 ID(所有查询必须包含供应商 ID)
- 唯一索引:保单号(
policy_no) - 支持查询:
- 供应商 ID + 订单号 → 查询该供应商下某订单的所有关联保单
- 供应商 ID + 保单号 → 精确查询单条保单
- 问题:供应商保单表存在严重数据倾斜(部分大供应商占绝大部分数据),影响查询性能与存储均衡。
3、目标
随着数据量增长至数百亿级,需对上述两套表实施冷热分离,仅保留最近1年内的保单作为热数据,其余归档为冷数据。
查询透明:应用层查询 不应感知冷热分离。
- 支持通过以下方式查询(无论保单冷热):
客户ID + 订单号客户ID + 保单号供应商ID + 订单号供应商ID + 保单号- 通过订单号查询
分页能力:
- 支持按照客户ID + 保单创建时间的 分页查询
- 支持按照供应商ID + 保单创建时间的 分页查询
技术选型限制:
- 可选用的技术栈包括:MySQL、Doris、ClickHouse、Elasticsearch 等。
- 需评估各方案在性能、成本、运维复杂度、扩展性等方面的权衡。
数据倾斜治理:冷库存储方案不得加剧或继承原供应商表的数据倾斜问题,应具备良好的数据分布能力。
二、选型评估
选 Doris
- 支持 高并发点查(Unique Key 模型)
- 支持 分区(Partition) + 分桶(Bucket),有效解决数据倾斜
- 支持 谓词下推、索引裁剪,查询效率高
- 社区活跃,国内有成熟案例(美团、京东、B站)
| 评估维度 | ClickHouse | Doris | Elasticsearch |
|---|---|---|---|
| 存储成本 | 压缩比 10:1,成本极低 | 压缩比 8:1,成本低 | 压缩比 5:1,成本中等 |
| 数据倾斜 | 哈希分片,彻底解决倾斜 | 哈希分桶,彻底解决倾斜 | 哈希分片,彻底解决倾斜,支持动态扩容调整 |
| 运维复杂度 | 需专业运维(集群扩容、数据均衡、参数调优),复杂度高 | 兼容MySQL语法,运维简单,中小团队易落地 |
需运维索引、分片、ILM策略,复杂度中等 |
| 扩展性 | 横向扩容简单,支持弹性伸缩,扩容无感知 | 分桶数固定,扩容需重新分桶,略复杂 | 支持动态扩缩容,分片可手动迁移,扩展性好 |
SQL兼容性 |
SQL语法差异大(如无 JOIN 优化、分页需手动计算),适配成本高 |
接近标准 SQL ,兼容 MySQL 语法,适配成本低 |
支持 SQL 查询(ES SQL),但功能有限,复杂查询需用DSL |
| 分页支持 | 需手动计算 OFFSET,大数据量分页效率下降,深分页性能差 |
原生支持 LIMIT/OFFSET,分页友好,效率稳定 |
支持分页,但大数据量(百万级+)分页效率较低 |
| 特色优势 | 极致吞吐,批量分析能力强,适合海量数据扫描 | 运维简单,SQL兼容,兼顾查询与同步灵活性 | 全文检索,多条件组合查询,近实时响应 |
三、热数据处理
- 保留策略:只保留
modify_time >= NOW() - INTERVAL 1 YEAR的数据 - 归档触发:每日凌晨执行归档任务(T-366 天数据)
- 删除策略:归档成功后,从 MySQL 物理删除(或先标记
is_archived=1,再异步清理)
ALTER TABLE qrxpolicy
ADD COLUMN archive_status TINYINT NOT NULL DEFAULT 0 COMMENT '0:未归档 1:已归档',
ADD INDEX idx_archive (archive_status, modified_date);
四、冷数据Doris
- 查询透明:应用层无需区分冷热数据,统一访问;
- 商家优先:高频商家查询(分页、导出)性能极致优化;
- 兼顾用户:支持用户侧低频点查(如
user_id + order_id); - 防数据倾斜:避免头部商家/用户导致集群负载不均;
- 成本可控:单表存储,避免冗余,便于运维。
| 原则 | 说明 |
|---|---|
| Doris 是唯一查询源 | 商家所有查询走 Doris |
| MySQL 是唯一写入源 | 所有数据变更(含冷数据更新)必须先写 MySQL |
| 归档状态由同步服务控制 | archive_status=1 表示“Doris 已确认写入成功” |
| 冷数据更新 = 复活 + 更新 + 同步 | 禁止直接写 Doris |
CREATE TABLE dws_policy_unified (
policy_no VARCHAR(64),
order_id VARCHAR(96),
user_id VARCHAR(50),
merchant_id INT,
-- 其他字段(保持不变)
shop_name VARCHAR(96),
insurance_id VARCHAR(20),
insurance_type VARCHAR(20),
cate_type VARCHAR(20),
sku STRING,
skucate3id STRING,
products_num INT,
premium DECIMAL(12,2),
risk_rate DECIMAL(12,4),
discount DECIMAL(12,2),
commission DECIMAL(12,2),
amount DECIMAL(12,2),
start_province INT,
end_province INT,
ship_from VARCHAR(500),
ship_to VARCHAR(500),
valid_time DATETIME,
claim_flag SMALLINT,
policy_status SMALLINT,
products_name STRING,
ext_info STRING,
pop_uuid1 VARCHAR(100),
pop_uuid2 VARCHAR(100),
fin_set_status INT,
fin_set_msg VARCHAR(512),
create_time DATETIME
) ENGINE=OLAP
UNIQUE KEY(policy_no)
-- ⭐ 关键调整:merchant_id 放第一!
ORDER BY(merchant_id, user_id, create_time DESC)
PARTITION BY RANGE(create_time) (
PARTITION p2020 VALUES LESS THAN ("2021-01-01"),
PARTITION p2021 VALUES LESS THAN ("2022-01-01"),
PARTITION p2022 VALUES LESS THAN ("2023-01-01"),
PARTITION p2023 VALUES LESS THAN ("2024-01-01"),
PARTITION p2024 VALUES LESS THAN ("2025-01-01"),
PARTITION p2025 VALUES LESS THAN ("2026-01-01"),
PARTITION p2026 VALUES LESS THAN ("2027-01-01")
)
DISTRIBUTED BY HASH(policy_no) BUCKETS 256
PROPERTIES(
"replication_num" = "3",
"enable_unique_key_merge_on_write" = "true",
"bloom_filter_columns" = "merchant_id,user_id,order_id,policy_no",
"bloom_filter_fpp" = "0.01"
);
1、关键设计
| 维度 | 配置 | 作用 |
|---|---|---|
| 主键模型 | UNIQUE KEY(policy_no) |
保单号全局唯一,支持精确更新与点查 |
| 排序键 | ORDER BY(merchant_id, user_id, create_time DESC) |
优先支持商家分页查询,其次是用户 |
| 分区策略 | PARTITION BY RANGE(create_time) |
按年分区,高效裁剪历史数据(冷热分离基础) |
| 分桶策略 | DISTRIBUTED BY HASH(policy_no) BUCKETS 256 |
倾斜:打散头部商家数据,避免热点 |
| 索引优化 | bloom_filter_columns = "merchant_id,user_id,order_id,policy_no" |
快速跳过无关数据块,提升点查效率 |
| 存储引擎 | enable_unique_key_merge_on_write = true |
写时合并,读性能最优(适合冷写热读) |
2、查询场景
| 查询模式 | 预估延迟 | 说明 |
|---|---|---|
merchant_id + 分页(按时间倒序) |
< 100ms | 物理连续存储,无 runtime sort |
merchant_id + order_id |
< 80ms | 排序键前缀 + Bloom Filter |
merchant_id + policy_no |
< 30ms | UNIQUE KEY 直接命中 |
user_id + order_id |
80~150ms | 全分片扫描,但 Bloom Filter 大幅过滤 |
order_id(单独) |
~100ms | Bloom Filter 过滤后扫描 |
policy_no(单独) |
< 20ms | UNIQUE KEY |
| 商家月度导出(10万+条) | < 10秒(用 EXPORT) |
分区裁剪 + 并行扫描 |
3、冷数据迁移
1)增量写入
-
发送变更发送
MQ -
消费者 直接写入
Doris,如担心并发,可加锁 -
写入成功后,执行:
UPDATE qrxpolicy SET archive_status = 1 WHERE policy_no = ?;
2)历史保单
- 数仓导出全量数据(
SELECT * FROM qrxpolicy) - 推送
MQ(全量模式) - 消费者批量写
Doris→ 成功后设archive_status = 1
5、冷数据更新
原则:复活 -> 更新 -> 同步
- 所有更新必须走 MySQL
- 已归档数据更新前,先插入 MySQL(
archive_status=0)
| 值 | 含义 | 说明 |
|---|---|---|
0 |
UNARCHIVED(未归档) | 数据在 MySQL 热库,可能活跃 |
1 |
ARCHIVED(已归档) | 数据已安全写入 Doris,可从 MySQL 删除 |
4、冷数据归档
1)首次上线归档(用 created_date)
- 终态保单 业务上不会再更新 → 即使
modified_date被 DDL 污染,也不会再变 created_date天然不可变
DELETE FROM qrxpolicy
WHERE archive_status = 1
AND created_date < '2023-01-01'
LIMIT 5000;
2)阶段 2:上线后日常归档(用 modified_date)
- 上线后,只有真实业务更新才会改
modified_date - 终态数据不会再更新 →
modified_date固定
DELETE FROM qrxpolicy
WHERE archive_status = 1
AND modified_date < '2024-01-01';


