前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

来自字节: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';   

ContactAuthor