在某运营商领域,话单、账单、用户行为等数据往往达到百亿甚至千亿级 ,传统单库单表无法支撑高并发读写和复杂查询,分库分表是解决该问题的核心方案。分库分表策略 、全局 ID 生成 、Sharding-Sphere 原理这三个是核心,尝试设计一个百亿级话单表的分库分表方案,并思考"如何高效地进行月度数据统计。
一、分库分表核心策略
分库分表的本质是将大表拆分为小表 、大库拆分为小库 ,分为垂直拆分 和水平拆分两大类,实际场景中通常结合使用。
1. 垂直拆分
垂直拆分是按照业务维度 或数据列维度进行拆分,适用于数据列多、部分列访问频繁的场景。
- 垂直分库 :按业务模块拆分,例如将用户库、话单库、账单库分开部署,降低单库压力。
- 优点:业务隔离,便于维护;降低单库的 IO 和存储压力。
- 缺点:跨库关联查询复杂,需通过分布式事务或中间件解决。
- 垂直分表 :将一张大表按列拆分为多张表,例如话单表中基础字段(通话时间、主被叫号码) 放在主表,扩展字段(通话地点、终端型号) 放在扩展表。
- 优点:减少单表列数,提升查询效率(减少 IO);高频字段单独存储,缓存命中率更高。
- 缺点:关联查询需 JOIN,增加开发复杂度。
2. 水平拆分
水平拆分是按照数据行维度 将数据分散到多个库 / 表中,所有拆分后的表结构完全一致,是处理海量数据的核心手段 。关键是选择分片键 和分片算法。
(1)分片键选择原则
分片键是水平拆分的依据,需满足:
- 高频查询条件(如话单表的
通话时间、主叫号码); - 数据分布均匀(避免数据倾斜);
- 符合业务访问模式(如运营商话单多按时间和用户查询)。
(2)核心分片算法
| 分片算法 | 原理 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 范围分片 | 按分片键的数值范围拆分(如按时间:202501、202502) | 时序数据(话单、日志) | 符合业务查询习惯,扩容简单 | 热点数据集中(如月末话单量突增) |
| 哈希分片 | 对分片键做哈希运算后取模(如用户 ID%16) | 用户维度查询(如按手机号查) | 数据分布均匀 | 扩容需重新哈希,数据迁移成本高 |
| 一致性哈希 | 将分片节点映射到哈希环,数据按分片键哈希后落到对应节点 | 分布式缓存 / 数据库扩容 | 扩容仅影响部分数据 | 实现复杂,易出现数据分布不均 |
| 复合分片 | 组合多个分片键(如先按时间范围分库,再按手机号哈希分表) | 多维度查询(话单的时间 + 用户) | 兼顾多场景查询 | 规则复杂,需中间件支持 |
(3)水平分库 vs 水平分表
- 水平分表:单库内拆分表,解决单表数据量过大问题,适用于并发不高但数据量大的场景。
- 水平分库:多库拆分表,解决单库的 IO / 连接数瓶颈,适用于高并发读写场景。
实际场景中通常采用分库 + 分表的双层拆分(如 16 个库,每个库 128 张表)。
3. 拆分后的关键问题
- 数据倾斜 :分片键选择不当导致部分库 / 表数据量过大(如按地区分片,一线城市话单量远高于其他地区),需通过复合分片 或动态调整分片规则解决。
- 跨库关联 :需通过反范式设计 (冗余字段)、分布式中间件 (如 Sharding-Sphere)或数据同步到数仓解决。
- 扩容 :范围分片扩容更友好(新增时间分片),哈希分片需通过预分片(如提前分 32/64 库)避免后期迁移。
二、全局 ID 生成方案
分库分表后,单库的自增 ID 无法保证全局唯一性,需设计全局唯一 ID 。运营商场景对 ID 的要求是:唯一、有序、高性能、高可用、支持追溯(如话单 ID 需关联时间和设备)。
1. 常见全局 ID 方案对比
| 方案 | 原理 | 优点 | 缺点 | 运营商场景适用性 |
|---|---|---|---|---|
| UUID/GUID | 基于随机数生成 128 位 ID | 生成简单,无中心节点 | 无序、占空间大、索引效率低 | 低(不适合有序查询) |
| 数据库自增 ID | 单独部署 ID 生成库,通过自增列生成 ID(如分表取模) | 有序、简单 | 性能瓶颈(单库 TPS 约 1 万)、单点故障 | 低(无法支撑高并发) |
| Redis 自增 ID | 利用 Redis 的 INCR/INCRBY 命令生成 ID,可按分片设置步长 | 高性能(TPS 约 10 万)、有序 | 需维护 Redis 集群,宕机可能导致 ID 重复(需持久化) | 中(适合中小并发) |
| 雪花算法(Snowflake) | 64 位 ID:1 位符号位 + 41 位时间戳 + 10 位机器 ID+12 位序列号 | 高性能(本地生成)、有序、含时间 | 时钟回拨会导致 ID 重复,需解决机器 ID 分配问题 | 高(运营商首选) |
| 美团 Leaf | 整合雪花算法和号段模式(Segment),号段模式预生成 ID 段缓存到本地 | 高性能、高可用、支持扩容 | 实现复杂,需部署服务 | 高(适合超大规模) |
| 百度 UidGenerator | 基于雪花算法优化,支持自定义时间戳、机器 ID 位数,解决时钟回拨问题 | 更灵活、适配分布式场景 | 依赖数据库,部署复杂 | 高(运营商定制化) |
2. 运营商话单 ID 的最优选择
运营商话单写入并发可达10 万 TPS 以上 ,推荐使用雪花算法 或美团 Leaf(Segment 模式):
- 雪花算法:本地生成 ID,无网络开销,41 位时间戳可记录约 69 年的时间,10 位机器 ID 可支持 1024 个节点,12 位序列号每毫秒可生成 4096 个 ID,完全满足话单高并发写入。
- 优化点:解决时钟回拨问题(如检测到时钟回拨则等待或切换序列号);将机器 ID 与运营商的机房 / 设备 ID 绑定,便于问题追溯。
示例雪花算法 ID 结构(运营商定制版):
sql
64位ID = 1位符号位 + 38位时间戳(精确到秒,支持约278年) + 12位机房ID(4096个机房) + 13位序列号(每秒8192个ID)
三、Sharding-Sphere 核心原理
Sharding-Sphere 是国内主流的分布式数据库中间件,包含Sharding-JDBC (客户端)、Sharding-Proxy (服务端)、Sharding-Scale (数据迁移)三大产品,其中Sharding-JDBC因高性能、低侵入性成为运营商场景的首选。
1. Sharding-Sphere 核心概念
- 逻辑表 :用户感知的表(如
t_call_record),对应多个物理表。 - 真实表 :实际存储数据的物理表(如
t_call_record_202501_001)。 - 分片键 :用于分片的字段(如
call_time、caller_num)。 - 绑定表:存在关联关系的表(如话单表和用户表),按相同分片规则拆分,可减少跨库关联。
- 广播表:全库同步的表(如字典表、地区表),无需分片。
2. Sharding-JDBC 核心流程
Sharding-JDBC 作为 JDBC 驱动的增强,在应用层完成分片逻辑,核心流程为:
sql
SQL解析 → 分片路由 → SQL改写 → 执行器执行 → 结果归并
- SQL 解析:将 SQL 解析为抽象语法树(AST),提取分片键、表名、条件等信息。
- 分片路由 :根据分片规则,计算 SQL 需要访问的物理库 / 表(如按
call_time=202501路由到db_01.t_call_record_202501_001)。 - SQL 改写:将逻辑表名改写为物理表名,补充分片条件。
- 执行器执行:通过线程池并行执行多个物理库的 SQL。
- 结果归并:将多个物理库的查询结果合并为统一结果集返回给应用。
3. 关键特性
- 读写分离:支持主库写入、从库查询,可配置从库负载均衡策略(轮询、随机)。
- 分布式事务:支持 XA 事务、Seata AT 事务,满足运营商核心业务的事务一致性要求。
- 数据加密:对敏感字段(如手机号、身份证号)进行透明加密,符合运营商数据安全规范。
- 影子库:支持 SQL 测试,避免生产环境风险。
四、百亿级话单表分库分表方案设计
1. 话单表业务特点分析
运营商话单表是典型的海量时序数据,核心特点:
- 数据量 :单月话单量可达100 亿 +,年数据量超千亿。
- 写入特性:高并发写入(峰值 TPS 达 10 万 +),几乎无更新操作(话单生成后只读)。
- 查询特性 :
- 高频查询:按主叫号码 + 时间范围查询个人话单;
- 低频查询:按被叫号码、地区、终端类型查询;
- 统计需求:月度 / 季度 / 年度的话单量、通话时长、流量使用等聚合统计。
- 数据生命周期 :话单数据需在线保存3 年,3 年后归档到冷存储(如 HDFS)。
2. 分库分表方案设计
(1)表结构定义
核心话单表t_call_record核心字段:
| 字段名 | 类型 | 说明 | 备注 |
|---|---|---|---|
| id | bigint | 全局唯一 ID | 雪花算法生成 |
| caller_num | varchar(20) | 主叫号码 | 分片键之一 |
| callee_num | varchar(20) | 被叫号码 | |
| call_time | datetime | 通话开始时间 | 分片键之一 |
| call_duration | int | 通话时长(秒) | |
| call_type | tinyint | 通话类型(语音 / 视频 / 流量) | |
| region_code | varchar(6) | 通话地区编码 | |
| create_time | datetime | 话单生成时间 |
(2)拆分策略
采用水平分库 + 水平分表 的复合分片 方案,结合时间范围 和手机号哈希,兼顾时序查询和用户维度查询。
-
分库维度 :按通话时间的月份进行范围分片,同时预留年维度扩展。
- 例如:2025 年 1 月的话单数据落入
db_call_202501,2025 年 2 月落入db_call_202502。 - 优势:符合话单的时序特性,月度统计可直接定位到对应库;历史数据归档时只需迁移整库,操作简单。
- 例如:2025 年 1 月的话单数据落入
-
分表维度 :在单月库内,按主叫号码的哈希值进行分片。
- 计算方式:
hash(caller_num) % 128,每个月库内拆分 128 张表,表名如t_call_record_000、t_call_record_001...t_call_record_127。 - 优势:主叫号码查询时可直接通过哈希定位到具体表,避免全表扫描;128 张表可将单表数据量控制在500 万 - 1000 万(MySQL 单表最优性能区间)。
- 计算方式:
-
分库分表规模计算:
- 单月 100 亿话单,每个月库拆分为 128 张表,单表数据量 = 100 亿 / 128≈7812 万(若需进一步降低单表数据量,可将分表数提升至 256)。
- 在线保存 3 年数据,需部署 36 个月份库(
db_call_202501~db_call_202712),后续通过冷热分离将超过 1 年的库迁移到低性能存储。
(3)全局 ID 设计
采用定制化雪花算法生成话单 ID,结构如下:
sql
64位ID = 1位符号位(0) + 39位时间戳(精确到秒,支持约178年) + 8位机房ID(256个机房) + 16位序列号(每秒65536个ID)
- 时间戳:包含通话时间,便于通过 ID 快速定位时间范围;
- 机房 ID:关联话单生成的机房,便于问题排查;
- 序列号:每秒可生成 65536 个 ID,满足 10 万 TPS 的写入需求。
(4)Sharding-Sphere 配置落地
使用Sharding-JDBC作为客户端分片中间件,核心配置(YAML 示例):
sql
shardingsphere:
rules:
sharding:
tables:
t_call_record: # 逻辑表
actual-data-nodes: db_call_$->{202501..202512}.t_call_record_$->{000..127} # 物理表节点
database-strategy: # 分库策略
standard:
sharding-column: call_time # 分库分片键
sharding-algorithm-name: call_time_range # 范围分片算法
table-strategy: # 分表策略
standard:
sharding-column: caller_num # 分表分片键
sharding-algorithm-name: caller_num_hash # 哈希分片算法
sharding-algorithms:
call_time_range: # 时间范围分片算法
type: INTERVAL
props:
datetime-pattern: yyyyMM
sharding-suffix-pattern: yyyyMM
begin-time: 2025-01-01 00:00:00
end-time: 2027-12-31 23:59:59
sharding-interval: 1 MONTH
caller_num_hash: # 手机号哈希分片算法
type: HASH_MOD
props:
sharding-count: 128 # 分表数
broadcast-tables: [t_region_dict] # 广播表(地区字典表)
props:
sql-show: true # 打印SQL,便于调试
(5)数据归档与冷热分离
- 热数据(近 1 年):部署在高性能 SSD 数据库集群,支持高并发查询;
- 温数据(1-3 年):部署在普通机械硬盘数据库集群,仅支持低频查询;
- 冷数据(3 年以上):归档到 HDFS/OSS,通过 Hive/ClickHouse 进行离线分析。
五、高效月度数据统计方案
百亿级话单表的月度统计(如单月通话总时长、用户平均通话次数、各地区话单量)若直接在业务库执行聚合查询,会导致:SQL 执行时间长 、占用业务库资源 、影响线上读写 。需采用预计算 + OLAP 分析的方案。
1. 方案一:预计算汇总表(实时统计)
(1)设计思路
基于分库分表的话单数据,创建月度统计汇总表 ,通过触发器 / 消息队列 / 定时任务实时更新汇总数据。
(2)汇总表设计
创建t_call_record_month_stat,按月份 + 地区 + 通话类型维度汇总:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| stat_month | varchar(6) | 统计月份(202501) |
| region_code | varchar(6) | 地区编码 |
| call_type | tinyint | 通话类型 |
| total_count | bigint | 话单总数 |
| total_duration | bigint | 总通话时长(秒) |
| user_count | bigint | 涉及用户数 |
| update_time | datetime | 最后更新时间 |
(3)数据更新方式
- 实时更新 :话单写入业务库后,通过Canal 监听 MySQL binlog,将话单数据发送到 Kafka,再由 Flink 消费 Kafka 消息,实时更新汇总表。
- 定时补全:每天凌晨执行一次定时任务,对当天的汇总数据进行校验和补全,确保数据准确性。
(4)优势
- 统计查询直接从汇总表读取,响应时间毫秒级;
- 不占用业务库资源,避免线上影响。
2. 方案二:基于 ClickHouse 的 OLAP 分析(复杂统计)
(1)设计思路
运营商的月度统计常涉及多维度复杂聚合 (如按用户等级、终端类型、通话时段统计),传统 MySQL 汇总表无法满足,需将话单数据同步到ClickHouse(专为海量时序数据聚合设计的 OLAP 数据库)。
(2)实施步骤
-
数据同步:通过 Canal 将业务库的话单 binlog 同步到 Kafka,再由 Flink 将 Kafka 数据清洗后写入 ClickHouse。
-
ClickHouse 表设计 :按月份 分表(MergeTree 引擎),分区键为
call_time,排序键为caller_num, call_time。sqlCREATE TABLE t_call_record_ck ( id UInt64, caller_num String, callee_num String, call_time DateTime, call_duration UInt32, call_type UInt8, region_code String ) ENGINE = MergeTree() PARTITION BY toYYYYMM(call_time) ORDER BY (caller_num, call_time); -
月度统计查询 :直接在 ClickHouse 执行聚合查询,性能比 MySQL 高 10-100 倍。
sql-- 统计202501各地区语音通话总时长 SELECT region_code, SUM(call_duration) AS total_duration FROM t_call_record_ck WHERE toYYYYMM(call_time) = 202501 AND call_type = 1 GROUP BY region_code;
(3)优势
- 支持多维度、复杂聚合查询,性能优异;
- 支持海量数据的实时分析,可应对运营商的灵活统计需求。
3. 方案三:Sharding-Sphere 联邦查询(轻量统计)
对于简单的月度统计,可使用Sharding-Sphere 联邦查询,由中间件自动并行查询各物理库 / 表,再汇总结果。
示例:
sql
-- 统计202501话单总数
SELECT COUNT(*) FROM t_call_record WHERE call_time BETWEEN '2025-01-01' AND '2025-01-31';
- Sharding-JDBC 会自动路由到
db_call_202501的 128 张表,并行执行COUNT(*),再将结果求和返回。 - 优势:无需额外组件,开发成本低;
- 缺点:仍会占用业务库资源,适合小数据量或低频统计。
4. 方案对比与选择
| 方案 | 适用场景 | 性能 | 开发成本 | 资源占用 |
|---|---|---|---|---|
| 预计算汇总表 | 固定维度的实时统计(如总话单量) | 毫秒级 | 中 | 低 |
| ClickHouse OLAP | 多维度复杂统计(如用户 + 地区 + 类型) | 秒级 | 高 | 中 |
| Sharding-Sphere 联邦 | 轻量、低频统计 | 分钟级 | 低 | 高 |
运营商最优选择 :预计算汇总表 + ClickHouse OLAP结合,固定维度统计用汇总表,复杂灵活统计用 ClickHouse。