1. 为什么我们需要分表分库?
当单表数据量突破千万级时,MySQL的性能曲线会突然变得陡峭。我清楚地记得第一次遇到这个问题的场景:一个用户行为日志表的查询响应时间从平均200ms骤增到3秒以上,即使加了索引也无济于事。这就是典型的数据量触发了MySQL的单机性能天花板。
1.1 单机MySQL的性能边界
MySQL在单表数据量500万行以内时表现良好,但超过这个阈值后会出现三个明显问题:
- 索引效率下降:B+树索引层级变深,查询时需要更多的磁盘I/O
- 维护成本飙升:ALTER TABLE操作可能锁表数小时
- 备份恢复困难:上百GB的单个表备份需要特殊处理
重要提示:不要等到性能出现问题时才开始考虑分表分库,应该在预估数据量达到单机处理能力的70%时就提前规划。
1.2 业务场景的驱动需求
不同业务对分表分库的需求差异很大:
- 电商订单系统:需要按用户ID分散查询压力
- 物联网设备数据:需要按时间维度管理历史数据
- 社交网络feed流:需要解决热点用户的数据倾斜问题
我经手的一个智能家居项目就遇到了典型的时间序列数据问题——设备每天产生2000万条状态记录,单机MySQL根本无法承受这种写入压力。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 分表分库的核心设计模式
2.1 水平分表(横向拆分)
把同一个表的数据按行拆分到不同的物理表中,是最常用的拆分方式。去年我们重构的物流系统就采用了这种方案:
sql复制-- 原始订单表
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
user_id INT,
order_data JSON,
created_at TIMESTAMP
);
-- 拆分后的订单表(按user_id哈希分10张表)
CREATE TABLE orders_0 (同原结构);
...
CREATE TABLE orders_9 (同原结构);
路由算法选择:
- 哈希取模:
table_suffix = user_id % 10 - 范围分片:按ID范围划分,如1-1000万在表1
- 一致性哈希:减少扩容时的数据迁移量
2.2 垂直分表(纵向拆分)
把宽表的列拆分到多个表中,适合解决"大宽表"问题。比如用户表可以拆分为:
sql复制-- 基础信息表
CREATE TABLE users_basic (
id INT PRIMARY KEY,
username VARCHAR(50),
password_hash CHAR(64)
);
-- 详细信息表
CREATE TABLE users_profile (
user_id INT PRIMARY KEY,
avatar_url VARCHAR(255),
bio TEXT,
FOREIGN KEY (user_id) REFERENCES users_basic(id)
);
这种拆分特别适合SSD存储环境,因为随机IO性能更好。
2.3 分库设计实战
当单机容量不足时,就需要考虑分库。我们去年实施的电商系统改造采用了"一主多从+分库"的架构:
code复制主库集群
├── 用户库 user_db(用户相关表)
├── 商品库 product_db
└── 订单库 order_db_0
├── 订单表 orders_00
├── ...
└── 订单表 orders_0N
跨库查询的解决方案:
- 字段冗余:在订单表中冗余商品名称等关键信息
- 内存聚合:分别查询后应用层合并结果
- 使用中间件:如MyCat、ShardingSphere
3. 分片键的选择艺术
选错分片键会导致灾难性后果。曾经有个项目用"订单创建时间"做分片键,结果发现每天90%的查询都集中在最近3天的分片上,完全失去了分库的意义。
3.1 优秀分片键的特征
- 离散性好:如用户ID比性别更适合
- 业务相关性高:常用查询条件应包含分片键
- 避免单调递增:如自增ID会导致写入热点
3.2 复合分片键策略
对于复杂场景,可以采用"主分片键+辅分片键"的组合:
java复制// 示例:先按商户ID分库,再按订单月份分表
int dbIndex = merchantId % dbCount;
int tableIndex = (yearMonth.hashCode() & Integer.MAX_VALUE) % tableCount;
4. 分库分表后的挑战与解决方案
4.1 分布式ID生成
自增ID在分布式环境下会冲突,我们最终选择了Snowflake方案:
python复制class Snowflake:
def __init__(self, worker_id):
self.worker_id = worker_id
self.sequence = 0
self.last_timestamp = -1
def next_id(self):
timestamp = time.time_ns() // 1_000_000
if timestamp < self.last_timestamp:
raise Exception("时钟回拨")
if timestamp == self.last_timestamp:
self.sequence = (self.sequence + 1) & 0xFFF
if self.sequence == 0:
timestamp = self.wait_next_millis()
else:
self.sequence = 0
self.last_timestamp = timestamp
return ((timestamp - 1288834974657) << 22) | (self.worker_id << 12) | self.sequence
4.2 跨分片查询优化
对于必须跨分片的查询(如全平台数据统计),我们采用以下策略:
- 并行查询:同时查询所有分片后聚合
- 汇总表:定期预生成统计结果
- 搜索引擎:将数据同步到Elasticsearch
4.3 事务一致性保障
分布式事务是个难题,我们最终采用了最终一致性方案:
- 本地事务+消息表
- 定时任务补偿
- 设计可重试的接口
5. 真实案例:日活千万的社交平台改造
去年主导的社交平台数据库改造项目,原始单表数据已达20亿条。我们的实施方案:
- 分库策略:按用户ID哈希分16个库
- 分表策略:每个库再按时间范围分12个月表
- 数据迁移:
- 双写过渡期2周
- 增量数据同步使用Canal
- 历史数据分批迁移
关键指标对比:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 查询P99延迟 | 1200ms | 85ms |
| 写入TPS | 1500 | 8500 |
| 备份时间 | 6小时 | 25分钟 |
这个项目让我深刻体会到:分库分表不是简单的技术选型,而是需要结合业务特点进行深度定制的系统工程。特别是在灰度发布阶段,我们不得不为某些特殊查询编写定制化的聚合逻辑。
