← 返回 MYSQL 列表

分表分库基础

分表分库基础

一、大表问题

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 分段
非分片键查询 映射表、冗余数据、广播查询
跨分片聚合 应用层合并、中间件支持
扩容 一致性哈希、双写迁移、停机迁移