分表分库基础
一、大表问题
1.1 大表的定义与问题
表数据量达到一定规模(通常单表超过 500万行 或 10GB)时,会出现以下问题:
| 问题 | 表现 |
|---|---|
| 查询慢 | 即使有索引,B+ 树层级加深,查询性能下降 |
| DDL 阻塞 | ALTER TABLE 重建表耗时极长(几十分钟到几小时) |
| 备份恢复慢 | mysqldump 和恢复时间都很长 |
| 锁竞争激烈 | 大量并发操作同一张表,锁冲突增加 |
| 维护困难 | 清理数据、迁移等操作成本高 |
sql
-- 查看表大小
SELECT
table_name,
ROUND(data_length / 1024 / 1024, 2) AS data_size_mb,
ROUND(index_length / 1024 / 1024, 2) AS index_size_mb,
ROUND((data_length + index_length) / 1024 / 1024, 2) AS total_size_mb,
table_rows
FROM information_schema.tables
WHERE table_schema = 'your_database'
AND table_name = 'orders';
1.2 大表问题解决思路
垂直扩展(升级硬件)
└── 加内存、换 SSD、升级 CPU
水平扩展(拆分数据)
├── 垂直分表:拆分字段到多张表
├── 水平分表:按行拆分到多张表
├── 分库:拆分到多个数据库实例
└── 分库分表:拆分到多个实例的多张表
二、垂直分表
2.1 拆分思路
将一张表的字段按访问频率和数据热度拆分到不同的表中。
原始表(orders):
id, order_no, user_id, amount, status, create_time,
pay_time, delivery_time, receiver_address, receiver_phone,
invoice_title, invoice_tax_no, remark, internal_memo
垂直拆分后:
主表(orders):高频字段
id, order_no, user_id, amount, status, create_time
扩展表(order_ext):低频/大字段
order_id, pay_time, delivery_time, receiver_address, receiver_phone
发票表(order_invoice):发票相关
order_id, invoice_title, invoice_tax_no, remark
内部表(order_internal):内部使用
order_id, internal_memo
2.2 垂直分表的优缺点
| 优点 | 缺点 |
|---|---|
| 单表体积变小,查询更快 | 需要跨表 JOIN |
| 访问频繁的字段集中在热表 | 事务复杂度增加 |
| 大字段不影响主表查询 | 需要处理数据一致性 |
| 冷数据可以存放到廉价存储 | 应用层需感知拆分 |
三、水平分表
3.1 拆分规则
sql
-- 拆分方式1:范围分表(按时间、ID范围)
orders_202401:2024年1月的数据
orders_202402:2024年2月的数据
orders_202403:2024年3月的数据
-- 拆分方式2:哈希分表(按某个字段哈希取模)
orders_0:user_id % 4 = 0
orders_1:user_id % 4 = 1
orders_2:user_id % 4 = 2
orders_3:user_id % 4 = 3
3.2 范围分表
sql
-- 按时间范围分表
-- 表名:orders_202401, orders_202402, ...
-- 插入时路由到对应的表
DELIMITER $$
CREATE PROCEDURE sp_insert_order(
p_order_no VARCHAR(32),
p_user_id BIGINT,
p_amount DECIMAL(12,2)
)
BEGIN
-- 根据当前月份决定插入哪张表
SET @table_name = CONCAT('orders_', DATE_FORMAT(NOW(), '%Y%m'));
SET @sql = CONCAT(
'INSERT INTO ', @table_name,
'(order_no, user_id, amount, create_time) VALUES (?, ?, ?, NOW())'
);
PREPARE stmt FROM @sql;
EXECUTE stmt USING p_order_no, p_user_id, p_amount;
DEALLOCATE PREPARE stmt;
END$$
DELIMITER ;
-- 查询时需要查询所有相关表
SELECT * FROM orders_202401 WHERE user_id = 100
UNION ALL
SELECT * FROM orders_202402 WHERE user_id = 100;
3.3 哈希分表
sql
-- 按 user_id 哈希取模分 4 张表
-- 计算路由:分表序号 = user_id % 4
-- user_id=100 → 100 % 4 = 0 → orders_0
-- user_id=101 → 101 % 4 = 1 → orders_1
-- 封装分表逻辑(应用层)
-- Java 示例
public class OrderShardingUtil {
private static final int TABLE_COUNT = 4;
public static String getTableName(Long userId) {
return "orders_" + (userId % TABLE_COUNT);
}
public static String getTableNameByOrderNo(String orderNo) {
// 也可以用 order_no 做哈希
int hash = Math.abs(orderNo.hashCode()) % TABLE_COUNT;
return "orders_" + hash;
}
}
3.4 水平分表的优缺点
| 优点 | 缺点 |
|---|---|
| 单表数据量可控,查询快 | 跨表查询/聚合困难 |
| 易于扩展(分片数可增加) | 分片键选择重要,影响查询 |
| 对应用透明(通过中间件) | 全局主键需要特殊处理 |
| 可分担写入压力 | 数据迁移/扩容复杂 |
四、分表后的关键问题
4.1 全局主键解决方案
sql
-- 方案1:使用数据库自增(分段)
-- 建一个全局 ID 生成表
CREATE TABLE global_id (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
PRIMARY KEY (id)
);
-- 应用层:先插入获取 ID,再插入分表
INSERT INTO global_id VALUES (NULL);
SELECT LAST_INSERT_ID();
-- 方案2:雪花算法(推荐)
-- Snowflake ID 结构:
-- 1bit(符号) + 41bit(毫秒时间戳) + 10bit(机器ID) + 12bit(序列号) = 64bit
-- 特点:
-- 全局唯一、趋势递增、无需依赖数据库
-- 方案3:使用 Redis incr
INCR global:order:id
-- 返回全局唯一 ID,配合前缀可生成业务 ID
4.2 跨分表查询
sql
-- 场景1:查询某个用户的所有订单(根据分片键查询)
-- 知道 user_id = 100,直接路由到 orders_0(100 % 4 = 0)
SELECT * FROM orders_0 WHERE user_id = 100 ORDER BY create_time DESC;
-- 场景2:按非分片键查询(全表扫描)
-- 只知道 order_no = '20240101001',不知道 user_id
-- 需要查询所有分表再合并
-- 应用层处理
-- Java 伪代码:
List<String> tableNames = Arrays.asList("orders_0", "orders_1", "orders_2", "orders_3");
List<Order> result = new ArrayList<>();
for (String tableName : tableNames) {
String sql = "SELECT * FROM " + tableName + " WHERE order_no = ?";
// 执行查询并合并结果
result.addAll(query(sql, orderNo));
}
4.3 数据迁移与扩容
sql
-- 场景:原分 4 张表,扩容到 8 张表
-- 原来:user_id % 4 → 4 张表
-- 现在:user_id % 8 → 8 张表
-- 迁移方案:
-- 方案1:停机迁移
-- 1. 停服,禁止写入
-- 2. 迁移数据:4 张表数据按新规则重新分配到 8 张表
-- 3. 切换配置,恢复服务
-- 方案2:双写迁移
-- 1. 开启双写(写入老表和新表)
-- 2. 同步历史数据(校验一致性)
-- 3. 切换读流量到新表
-- 4. 下线老表
-- 方案3:使用一致性哈希减少迁移量
-- 一致性哈希:扩容时只迁移 1/n 的数据(n 是新节点数)
五、实战案例
案例:订单系统水平分表设计
sql
-- 需求:订单表预计年增长 1 亿行,需要水平分表
-- 分表策略:
-- 分片键:user_id
-- 分表数量:64 张(orders_0 ~ orders_63)
-- 规则:user_id % 64
-- 创建分表(可以使用模板脚本生成)
CREATE TABLE orders_0 (
id BIGINT NOT NULL COMMENT '雪花算法生成的ID',
order_no VARCHAR(32) NOT NULL COMMENT '订单号',
user_id BIGINT NOT NULL COMMENT '用户ID',
amount DECIMAL(12,2) NOT NULL DEFAULT 0.00,
status TINYINT NOT NULL DEFAULT 0,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
INDEX idx_user_id (user_id),
INDEX idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ... orders_1 ~ orders_63(同结构)
-- 建立订单号 → user_id 的映射表(解决非分片键查询)
CREATE TABLE order_user_mapping (
order_no VARCHAR(32) PRIMARY KEY,
user_id BIGINT NOT NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 插入时同时写入映射表
INSERT INTO order_user_mapping (order_no, user_id) VALUES (?, ?);
-- 查询时先查映射表获取 user_id,再路由到具体分表
SELECT user_id FROM order_user_mapping WHERE order_no = '20240101001';
-- 得到 user_id = 100
-- 路由到 orders_0(100 % 64 = 36) ← 取模结果看具体实现
案例:日志表按时间分表 + 自动归档
sql
-- 需求:操作日志表每天约 100 万行,按月分表
-- 每月自动创建下个月的表
DELIMITER $$
CREATE PROCEDURE sp_create_next_month_table()
BEGIN
DECLARE v_next_month VARCHAR(6);
DECLARE v_sql TEXT;
SET v_next_month = DATE_FORMAT(DATE_ADD(NOW(), INTERVAL 1 MONTH), '%Y%m');
SET v_sql = CONCAT(
'CREATE TABLE IF NOT EXISTS operation_log_', v_next_month, ' (',
'id BIGINT NOT NULL AUTO_INCREMENT,',
'user_id BIGINT NOT NULL,',
'action VARCHAR(100) NOT NULL,',
'detail TEXT,',
'ip VARCHAR(45),',
'create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,',
'PRIMARY KEY (id),',
'INDEX idx_user_id (user_id),',
'INDEX idx_create_time (create_time)',
') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'
);
PREPARE stmt FROM v_sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
END$$
-- 每月 28 日创建下个月的表
CREATE EVENT ev_create_log_table
ON SCHEDULE EVERY 1 MONTH
STARTS '2024-01-28 02:00:00'
DO
CALL sp_create_next_month_table();
总结
| 拆分方式 | 适用场景 | 核心问题 |
|---|---|---|
| 垂直分表 | 表字段多(>30)、有大字段 | 跨表 JOIN、事务一致性问题 |
| 水平分表 | 单表数据量超大(>千万) | 分片键选择、跨分片查询、全局主键 |
| 范围分表 | 时间维度数据(日志、订单) | 数据分布可能不均 |
| 哈希分表 | 用户维度数据 | 扩容需数据迁移 |
| 关键问题 | 解决方案 |
|---|---|
| 全局主键 | 雪花算法、Redis incr、DB 分段 |
| 非分片键查询 | 映射表、冗余数据、广播查询 |
| 跨分片聚合 | 应用层合并、中间件支持 |
| 扩容 | 一致性哈希、双写迁移、停机迁移 |