MySQL 8.0 分库分表架构与实战指南
一、分库分表概述
1. 为什么需要分库分表
MySQL 8.0 虽然在性能、功能上持续增强,但面对海量数据和高并发场景时,单库单表架构仍然会遇到以下瓶颈:
| 瓶颈类型 | 表现形式 | 触发阈值 |
|---|---|---|
| 数据量瓶颈 | 单表数据量超过2000万行或文件大小超过50GB | 查询性能从毫秒级降至秒级 |
| 并发瓶颈 | 单实例QPS持续超过8000(读写混合场景)或纯写QPS超过3000 | 连接数耗尽,行锁竞争加剧 |
| 存储瓶颈 | 单库存储空间不足,备份恢复耗时过长 | 影响业务可用性和数据安全 |
| 性能瓶颈 | 高频SQL执行时间超1秒,索引优化已无法满足需求 | 用户体验下降 |
2. 分库分表的核心目标
- 分散存储压力:将大表拆分为小表,大库拆分为小库,避免单点过载
- 提升并发能力:多库多表并行处理请求,突破单库单表的性能上限
- 优化运维效率:小表备份/恢复、扩容/缩容更高效,故障影响范围更小
- 支持业务水平扩展:按需增加数据库节点,无需重构底层架构
二、分库分表核心概念
1. 垂直拆分vs水平拆分
垂直拆分:按业务领域或字段维度拆分
- 垂直分库:将不同业务模块的数据表拆分到不同数据库(用户库、订单库、商品库)
- 垂直分表:将一张大表按字段访问频率拆分为多张表(基础表、扩展表)
水平拆分:按数据行维度拆分
- 水平分库:将同一表的数据分布到多个数据库实例
- 水平分表:将同一表的数据分布到同一数据库的多张物理表中
| 特性 | 垂直拆分 | 水平拆分 |
|---|---|---|
| 核心逻辑 | 按业务模块/字段维度拆分 | 按数据行维度拆分 |
| 适用场景 | 业务模块解耦、表字段过多 | 数据量超大、并发高 |
| 解决痛点 | 降低单库压力、减少IO数据量 | 突破单表性能瓶颈 |
| 扩展性 | 有限(需业务重新划分) | 无限(线性扩展) |
| 复杂度 | 跨库JOIN复杂 | 跨分片查询复杂 |
2. 分片策略类型
2.1 取模分片(Hash Sharding)
sql
-- 核心公式
分片序号 = hash(分片键) % 分片总数
-- 示例:按user_id取模分到4个库、16个表
库索引 = user_id % 4
表索引 = user_id % 16
优点 :数据分布均匀、查询效率高(直接通过分片键计算分片)
缺点 :扩容困难(需重新哈希迁移数据)
适用场景:数据量可预估、长期稳定的业务(如用户表、商品表)
2.2 范围分片(Range Sharding)
sql
-- 按时间范围分片
PARTITION p202401 VALUES LESS THAN (TO_DAYS('2024-05-01'))
PARTITION p202402 VALUES LESS THAN (TO_DAYS('2024-06-01'))
-- 按数值范围分片
user_1_1000000: 存储1-100万用户
user_1000001_2000000: 存储100万-200万用户
优点 :扩容简单(直接新增分片)、支持范围查询高效
缺点 :可能存在热点分片(如当前月份数据量最大)
适用场景:时间序列数据(订单、日志)、数据量持续增长的场景
2.3 列表分片(List Sharding)
sql
-- 按地区枚举分片
order_bj: 存储北京地区订单
order_sh: 存储上海地区订单
order_gz: 存储广州地区订单
优点 :业务逻辑清晰、查询针对性强
缺点 :分片数量固定、可能存在数据分布不均
适用场景:分片键枚举值较少且固定(如地区、业务线、状态)
2.4 一致性哈希分片(Consistent Hashing)
java
// 将哈希空间组织成环形结构
// 数据的哈希值落在环上后,顺时针找到第一个节点
// 引入虚拟节点机制,提升数据分布均匀性
优点 :扩容时仅需迁移部分数据,无需全量重算
缺点 :实现复杂,范围查询需要全分片扫描
适用场景:需要频繁扩容、节点动态变化的分布式系统
三、MySQL 8.0 分区表实战
MySQL 8.0 原生支持分区表,是在单实例内实现水平拆分的轻量级方案。
1. 分区表类型
1.1 RANGE 分区(范围分区)
sql
CREATE TABLE orders (
order_id BIGINT NOT NULL AUTO_INCREMENT,
user_id INT NOT NULL,
amount DECIMAL(12,2) NOT NULL,
order_date DATE NOT NULL,
status TINYINT DEFAULT 1,
PRIMARY KEY (order_id, order_date),
KEY idx_user (user_id, order_date)
) ENGINE=InnoDB
PARTITION BY RANGE (YEAR(order_date)) (
PARTITION p2022 VALUES LESS THAN (2023),
PARTITION p2023 VALUES LESS THAN (2024),
PARTITION p2024 VALUES LESS THAN (2025),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
使用场景 :按时间范围查询频繁的订单、日志类数据
优化技巧:
- 分区键必须包含在主键中
- 为非分区键创建全局二级索引(MySQL 8.0.16+支持)
1.2 HASH 分区(哈希分区)
sql
CREATE TABLE employees (
id INT NOT NULL,
fname VARCHAR(30),
lname VARCHAR(30),
hired DATE NOT NULL DEFAULT '1970-01-01',
job_code INT,
store_id INT,
PRIMARY KEY (id)
) ENGINE=InnoDB
PARTITION BY HASH(store_id) PARTITIONS 4;
使用场景 :用户、商品等需要数据均匀分布的表
注意:使用LINEAR HASH可减少扩容时的数据迁移量
1.3 复合分区(一级分区+二级子分区)
sql
CREATE TABLE order_main (
`id` bigint NOT NULL AUTO_INCREMENT,
`order_no` varchar(64) NOT NULL,
`user_id` bigint NOT NULL,
`status` tinyint NOT NULL COMMENT '1:待支付 2:已支付 3:已取消',
`create_time` datetime NOT NULL,
`pay_time` datetime DEFAULT NULL,
`total_amount` decimal(12,2) NOT NULL,
PRIMARY KEY(`id`, `create_time`, `status`),
KEY `idx_user`(`user_id`, `create_time`)
) ENGINE=InnoDB
PARTITION BY RANGE (YEAR(create_time)) -- 一级:按年分区
SUBPARTITION BY LIST (status) -- 二级:按状态子分区
SUBPARTITIONS 3 (
PARTITION p2023Q1 VALUES LESS THAN (TO_DAYS('2023-04-01')) (
SUBPARTITION p2023Q1_s1 VALUES IN(1),
SUBPARTITION p2023Q1_s2 VALUES IN(2),
SUBPARTITION p2023Q1_s3 VALUES IN(3)
),
PARTITION p2023Q2 VALUES LESS THAN (TO_DAYS('2023-07-01')) (
SUBPARTITION p2023Q2_s1 VALUES IN(1),
SUBPARTITION p2023Q2_s2 VALUES IN(2),
SUBPARTITION p2023Q2_s3 VALUES IN(3)
)
);
优势 :按时间归档(一级分区) + 同一年内数据均匀分布(二级分区)
查询优化 :WHERE created_at = '2024-06-01' AND user_id = 12345 仅扫描1个子分区
2. 分区表管理操作
2.1 添加新分区
sql
-- 为订单表添加2024年5月分区
ALTER TABLE orders
ADD PARTITION (
PARTITION p202405 VALUES LESS THAN (TO_DAYS('2024-06-01'))
);
2.2 删除旧分区
sql
-- 归档2023年数据
ALTER TABLE orders DROP PARTITION p2023;
2.3 查看分区数据分布
sql
SELECT
PARTITION_NAME,
TABLE_ROWS
FROM
INFORMATION_SCHEMA.PARTITIONS
WHERE
TABLE_NAME = 'orders'
AND TABLE_SCHEMA = 'your_db';
2.4 分区表限制与注意事项
分区数量限制:
- MySQL 8.0分区数建议不超过1024个(过多导致元数据管理开销增大)
- 单分区数据量控制在100万-1000万行(根据服务器配置调整)
分区键约束:
- 分区键必须包含在所有唯一索引中(包括主键)
- 优先选择查询频率高的过滤字段
- 避免使用更新频繁的字段作为分区键
适用场景判断:
| 场景 | 推荐方案 | 不推荐方案 |
|---|---|---|
| 时间序列数据(订单、日志) | RANGE分区 | HASH分区 |
| 点查为主(用户ID、商品ID) | HASH分区 | RANGE分区 |
| 地理维度查询 | LIST分区 | HASH/RANGE |
3. 分区表 vs 分布式分库分表对比
| 特性 | 内置分区表 | 分布式分库分表 |
|---|---|---|
| 跨节点事务 | 支持ACID | 复杂(需分布式事务) |
| 部署复杂度 | 低(单实例) | 高(多节点集群) |
| 扩展性 | 有限(单实例瓶颈) | 无限(水平扩展) |
| 应用改造 | 无(透明访问) | 需改造(引入中间件) |
| 适用场景 | 中大规模数据(亿级以下) | 超大规模数据(十亿级以上) |
四、中间件分库分表方案
1. MyCat vs ShardingSphere 对比
| 对比维度 | MyCat | ShardingSphere(推荐) |
|---|---|---|
| 架构模式 | 服务端分片(代理层) | 客户端分片(JDBC) |
| 性能 | 较高(多一次网络跳转) | 极高(直连数据库) |
| 延迟 | 较高(代理转发开销) | 极低(本地处理SQL路由) |
| SQL兼容性 | 较好(模拟MySQL协议) | 优秀(内置ANTLR解析引擎) |
| 功能特性 | 专注分片、读写分离 | 分库分表+读写分离+分布式事务+数据加密等一站式解决方案 |
| 应用侵入性 | 无(透明访问) | 低(需引入依赖) |
| 运维成本 | 高(需维护中间件集群) | 低(随应用发布) |
| 社区活跃度 | 一般 | 极高(Apache顶级项目) |
选型建议:
- 新系统/微服务架构: 推荐ShardingSphere-JDBC
- 遗留系统改造/非Java技术栈: 推荐MyCat(零侵入、语言无关)
2. ShardingSphere-JDBC 配置实战
2.1 数据源配置
yaml
spring:
shardingsphere:
datasource:
names: ds0, ds1, ds2, ds3
ds0:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.1.100:3306/order_db?useSSL=false&characterEncoding=UTF-8
username: root
password: password
ds1:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.1.101:3306/order_db?useSSL=false&characterEncoding=UTF-8
username: root
password: password
2.2 分片规则配置
yaml
spring:
shardingsphere:
rules:
orders:
actual-data-nodes: ds$->{0..3}.orders_$->{0..7}
table-strategy:
standard:
sharding-column: user_id
sharding-algorithm-name: order-table-mod
key-generator:
column: order_id
type: SNOWFLAKE
sharding-algorithms:
order-table-mod:
type: MOD
props:
sharding-count: 8
key-generators:
snowflake:
type: SNOWFLAKE
props:
worker-id: 1
max-tolerance: 100
2.3 分片算法实现
java
// 取模分片算法
public class OrderTableModAlgorithm implements PreciseShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Long> shardingValue) {
Long orderId = shardingValue.getValue();
// 按user_id取模,实际路由时传入user_id
long userId = getUserIdFromOrderId(orderId);
return "ds" + (userId % 4) + ".orders_" + (userId % 8);
}
}
// 一致性哈希分片算法
public class ConsistentHashAlgorithm implements ComplexKeysShardingAlgorithm<String, Long> {
private final TreeMap<Long, String> virtualNodes = new TreeMap<>();
public ConsistentHashAlgorithm(int virtualNodeCount) {
// 初始化虚拟节点
for (int i = 0; i < physicalNodeCount; i++) {
for (int j = 0; j < virtualNodeCount / physicalNodeCount; j++) {
long hash = md5("ds" + i + "_" + j);
virtualNodes.put(hash, "ds" + i);
}
}
}
}
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<Long> shardingValue) {
Long hash = md5(shardingValue.getValue().toString());
// 找到顺时针第一个节点
Map.Entry<Long, String> entry = virtualNodes.ceilingEntry(hash);
return Collections.singleton(entry.getValue());
}
}
3. MyCat 分库分表配置
3.1 schema.xml 主配置
xml
<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
<!-- 逻辑库配置 -->
<schema name="order_db" checkSQLschema="false" sqlMaxLimit="1000">
<table name="orders" dataNode="dn_orders" rule="order_rule" />
</schema>
<!-- 数据节点 -->
<dataNode name="dn_orders" dataHost="mysql_cluster" database="order_db" />
<!-- 数据主机 -->
<dataHost name="mysql_cluster"
maxCon="1000" minCon="10"
balance="1" writeType="0" dbType="mysql" dbDriver="native"
switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<!-- 主库配置 -->
<writeHost host="hostM1" url="192.168.1.100:3306" user="mycat_user" password="MycatPass@123">
<!-- 从库1 -->
<readHost host="slave1" url="192.168.1.101:3306" user="mycat_user" password="MycatPass@123" />
<!-- 从库2 -->
<readHost host="slave2" url="192.168.1.102:3306" user="mycat_user" password="MycatPass@123" />
</writeHost>
<!-- 备用主库 -->
<writeHost host="hostM2" url="192.168.1.103:3306" user="mycat_user" password="MycatPass@123">
<readHost host="slave3" url="192.168.1.104:3306" user="mycat_user" password="MycatPass@123" />
</writeHost>
</dataHost>
</mycat:schema>
3.2 rule.xml 分片规则
xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mycat:rule SYSTEM "rule.dtd">
<mycat:rule xmlns:mycat="http://io.mycat/">
<!-- 订单表分片规则 -->
<tableRule name="order_rule">
<rule>
<columns>user_id</columns>
<algorithm>order_inline</algorithm>
</rule>
</tableRule>
<!-- 取模分片算法 -->
<function name="order_inline"
class="io.mycat.route.function.PartitionByMod">
<property name="count">4</property> <!-- 分库数 -->
</function>
</mycat:rule>
五、垂直拆分实战案例
1. 电商系统垂直分库方案
1.1 业务模块划分
原始单体库: ecommerce_db
拆分后:
├── user_db (用户中心库)
│ ├── user (用户基础信息)
│ ├── user_address (收货地址)
│ ├── user_account (账户安全)
│ └── user_behavior (用户行为日志)
│
├── order_db (交易中心库)
│ ├── orders (订单主表)
│ ├── order_items (订单明细)
│ ├── payments (支付记录)
│ └── logistics (物流信息)
│
├── product_db (商品中心库)
│ ├── products (商品基础信息)
│ ├── categories (商品分类)
│ ├── inventory (库存信息)
│ └── skus (商品规格)
│
└── pay_db (支付中心库)
├── pay_records (支付流水)
└── refunds (退款记录)
1.2 垂直分表案例
sql
-- 拆分前(单表包含12字段)
CREATE TABLE user (
id BIGINT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
password VARCHAR(64) NOT NULL,
phone VARCHAR(20) NOT NULL,
email VARCHAR(100),
birthday DATE,
gender TINYINT,
avatar_url VARCHAR(255),
personal_profile TEXT,
registration_time DATETIME NOT NULL,
last_login_time DATETIME,
account_status TINYINT NOT NULL
);
-- 拆分后
-- 基础信息表(高频访问)
CREATE TABLE user_basic (
id BIGINT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
phone VARCHAR(20) NOT NULL,
email VARCHAR(100),
gender TINYINT,
registration_time DATETIME NOT NULL,
last_login_time DATETIME,
account_status TINYINT NOT NULL,
KEY idx_phone (phone)
);
-- 扩展信息表(低频访问)
CREATE TABLE user_extend (
id BIGINT PRIMARY KEY,
avatar_url VARCHAR(255),
personal_profile TEXT,
security_question VARCHAR(100),
security_answer VARCHAR(100),
KEY idx_reg_time (registration_time)
);
拆分原则:
- 将高频查询字段集中在主表
- 将大字段(TEXT/BLOB)独立存储,避免行溢出
- 两表通过id关联,保持数据一致性
- 避免过度拆分(增加JOIN复杂度)
2. 用户表垂直拆分优化效果
| 优化项 | 拆分前 | 拆分后 |
|---|---|---|
| 单表字段数 | 12个字段 | 基础表6个 + 扩展表6个 |
| 单行数据大小 | 约1.5KB(含TEXT字段) | 基础表约200B |
| 登录查询IO | 读取完整1.5KB | 仅读取200B(提升7.5倍) |
| 索引树深度 | 4层 | 3层(减少1次IO) |
六、水平拆分实战案例
1. 电商订单系统混合拆分方案
1.1 架构设计
订单库: order_db
垂直拆分:订单相关模块
水平拆分:按user_id哈希 + 按时间范围
拆分策略:
分库: 4个数据库实例(order_db_0 ~ order_db_3)
分表: 每个库8张表(orders_0 ~ orders_7)
总计: 32张物理表
分片键: user_id(高频查询字段,分布均匀)
1.2 分片规则配置
yaml
# ShardingSphere配置
sharding:
tables:
orders:
# 4个库 × 8个表 = 32个物理节点
actual-data-nodes: order_db_$->{0..3}.orders_$->{0..7}
# 分库策略
database-strategy:
standard:
sharding-column: user_id
sharding-count: 4
sharding-algorithm-name: database-mod
# 分表策略
table-strategy:
standard:
sharding-column: user_id
sharding-count: 8
sharding-algorithm-name: table-mod
# 全局ID生成
key-generator:
column: order_id
type: SNOWFLAKE
props:
worker-id: 1
max-tolerance: 100
1.3 数据分布示例
用户ID=1001的分片路径:
分库索引 = 1001 % 4 = 1 → order_db_1
表索引 = 1001 % 8 = 1 → orders_1
最终路由: order_db_1.orders_1
用户ID=1002的分片路径:
分库索引 = 1002 % 4 = 2 → order_db_2
表索引 = 1002 % 8 = 6 → orders_6
最终路由: order_db_2.orders_6
2. 关联表处理策略
2.1 同分片键关联(订单明细表)
sql
-- 与订单表使用相同分片键,确保关联数据在同一库同表
CREATE TABLE order_items (
id BIGINT PRIMARY KEY,
order_id BIGINT NOT NULL, -- 分片键
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
price DECIMAL(10,2) NOT NULL,
KEY idx_order_id (order_id)
) ENGINE=InnoDB;
2.2 冗余字段关联(用户信息冗余)
sql
-- 订单表冗余用户基本信息,避免JOIN查询
CREATE TABLE orders (
id BIGINT PRIMARY KEY,
order_no VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
user_name VARCHAR(50), -- 冗余字段
user_phone VARCHAR(20), -- 冗余字段
amount DECIMAL(12,2) NOT NULL,
create_time DATETIME NOT NULL,
KEY idx_user (user_id, create_time)
) ENGINE=InnoDB;
冗余策略:
- 冗余低频变更字段(用户名、手机号)
- 冗余仅查询字段(显示订单列表时需要)
- 冗余字段不参与业务逻辑判断
3. 跨分片查询处理
3.1 广播表(数据字典)
yaml
# ShardingSphere配置:全局表配置
sharding:
tables:
dict_codes:
# 广播到所有分片
actual-data-nodes: ds0.dict_codes, ds1.dict_codes, ds2.dict_codes, ds3.dict_codes
key-generator:
column: id
type: SNOWFLAKE
3.2 绑定表(关联表固定分片)
sql
-- 通过业务逻辑将订单明细与订单固定在同一分片
CREATE TABLE order_binding (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
order_id BIGINT NOT NULL,
bind_time DATETIME NOT NULL,
KEY idx_user_order (user_id, order_id)
) ENGINE=InnoDB;
-- 查询时先通过绑定表定位分片
-- 再在目标分片内查询明细数据
4. 跨分片分页优化
4.1 基于分片键的分页(推荐)
java
// 优势:直接定位目标分片,避免全分片扫描
List<Order> queryByUserId(Long userId, int page, int size) {
int shardIndex = userId % 4;
String tableName = "orders_" + (userId % 8);
// SQL: SELECT * FROM orders WHERE user_id = ? LIMIT ?, ?
// 仅在目标分片执行分页
return jdbcTemplate.query(
"SELECT * FROM orders_" + shardIndex + " WHERE user_id = ? LIMIT ?, ?",
userId, (page - 1) * size, size
);
}
4.2 全分片扫描分页(需谨慎使用)
java
// 场景:不带分片键条件的查询
// 劣势:需要访问所有分片,性能较差
List<Order> queryByStatus(int status, int page, int size) {
List<Order> allResults = new ArrayList<>();
// 遍历所有分片
for (int dbIndex = 0; dbIndex < 4; dbIndex++) {
for (int tableIndex = 0; tableIndex < 8; tableIndex++) {
String tableName = "orders_" + tableIndex;
List<Order> shardResults = jdbcTemplate.query(
"SELECT * FROM orders WHERE status = ? LIMIT ?, ?",
status, (page - 1) * size, size
);
allResults.addAll(shardResults);
}
}
// 应用层归并排序
return sortAndPaginate(allResults, page, size);
}
性能分析:
- 假设32个分片,每页10条数据
- 全分片扫描需要获取320条数据(32 × 10)
- 应用层归并排序内存消耗320条
- 偏移量越大,网络传输数据越多
七、分布式ID生成方案
1. 雪花算法(Snowflake)
java
public class SnowflakeIdGenerator {
// 起始时间戳(2024-01-01 00:00:00)
private final long START_TIMESTAMP = 1704060800000L;
// 各部分位数
private final long WORKER_ID_BITS = 5L; // 32个工作节点
private final long DATACENTER_ID_BITS = 5L; // 32个数据中心
private final long SEQUENCE_BITS = 12L; // 每毫秒生成4096个ID
private final long WORKER_ID_SHIFT = SEQUENCE_BITS;
private final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
private final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS;
private long workerId;
private long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long workerId, long datacenterId) {
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long currentTimestamp = System.currentTimeMillis();
// 同一毫秒内序列号自增
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & ((1L << SEQUENCE_BITS) - 1);
} else {
sequence = 0L;
}
lastTimestamp = currentTimestamp;
// 组装64位ID
return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
| (datacenterId << DATACENTER_ID_SHIFT)
| (workerId << WORKER_ID_SHIFT)
| sequence;
}
}
// ID结构解析:便于快速定位分片
public class IdParser {
public static long getShardingKey(long snowflakeId) {
// 提取workerId部分作为分片键
return (snowflakeId >> WORKER_ID_SHIFT) & ((1L << WORKER_ID_BITS) - 1);
}
}
ID结构解析:
| 部分 | 位数 | 值 | 说明 |
|------|------|------|------|
| 时间戳 | 41位 | 0 ~ 2^41-1 | 约69年 |
| 数据中心ID | 5位 | 0 ~ 31 | 32个数据中心 |
| 工作节点ID | 5位 | 0 ~ 31 | 32个工作节点 |
| 序列号 | 12位 | 0 ~ 4095 | 每毫秒4096个ID |
2. 数据库自增序列
sql
-- 方案1:不同库使用不同步长
-- 库0: 1, 3, 5, 7, 9, ... (步长2,起始值1)
-- 库1: 2, 4, 6, 8, 10, ... (步长2,起始值2)
CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
amount DECIMAL(12,2) NOT NULL,
create_time DATETIME NOT NULL
) ENGINE=InnoDB;
-- 初始化序列(MySQL 8.0+)
CREATE SEQUENCE IF NOT EXISTS order_seq START WITH 1 INCREMENT BY 2;
八、高可用与运维实践
1. 数据迁移与扩容
1.1 双写扩容(无停机)
扩容流程:
1. 准备新分片服务器(数据库配置与原库完全一致)
2. 在应用层配置双写(新库+旧库)
3. 数据同步期间双写,确保数据一致性
4. 同步完成后验证数据一致性(对比checksum)
5. 切换流量到新库(灰度发布)
6. 下线旧库,删除冗余数据
1.2 预分片扩容
yaml
# 提前规划足够的分片数量
sharding:
tables:
orders:
# 预分1024张表
actual-data-nodes: order_db_$->{0..3}.orders_$->{0..255}
# 初始分布在4个库,每个库256张表
# 扩容时只需迁移部分表到新库,无需修改分片规则
优势:
- 扩容无需修改分片规则(应用层零感知)
- 数据迁移以表为单位,可独立控制迁移节奏
- 支持渐进式扩容(分批迁移)
2. 监控与告警
2.1 核心监控指标
| 指标类型 | 监控项 | 告警阈值 |
|---|---|---|
| 性能监控 | QPS、TPS、慢查询数、平均响应时间 | QPS下降>30% 或 慢查询数>阈值×2 |
| 分片监控 | 各分片数据量分布、分片访问热点 | 单分片数据量>平均值50% |
| 连接监控 | 连接池使用率、活跃连接数 | 连接池使用率>80% |
| 复制监控 | 主从延迟、同步状态 | 延迟>5秒或IO线程停止 |
2.2 分片数据倾斜检测
sql
-- 检测各分片数据量分布
SELECT
SUBSTRING_INDEX(TABLE_NAME, -1) as db_index,
TABLE_ROWS as row_count
FROM
INFORMATION_SCHEMA.PARTITIONS
WHERE
TABLE_SCHEMA = 'order_db'
AND TABLE_NAME LIKE 'orders_%'
ORDER BY TABLE_ROWS DESC;
-- 计算标准差
WITH avg_size AS (
SELECT AVG(TABLE_ROWS)
FROM INFORMATION_SCHEMA.PARTITIONS
WHERE TABLE_SCHEMA = 'order_db'
)
SELECT
TABLE_NAME,
TABLE_ROWS,
TABLE_ROWS - avg_size as diff,
ABS(TABLE_ROWS - avg_size) / avg_size * 100 as diff_percent
FROM INFORMATION_SCHEMA.PARTITIONS
WHERE TABLE_SCHEMA = 'order_db'
ORDER BY diff_percent DESC;
倾斜处理:
- 数据量差异>30%:自动告警,考虑重新分片
- Top分片数据量>2倍:手动触发数据重平衡
- 定期归档历史数据:释放Top分片压力
3. 故障排查与优化
3.1 分片失效问题
sql
-- 问题:查询条件中分片键使用函数
-- 错误示例
SELECT * FROM orders
WHERE YEAR(create_time) = 2024; -- 函数包裹,无法触发分区剪枝
-- 正确示例
SELECT * FROM orders
WHERE create_time BETWEEN '2024-01-01' AND '2024-12-31'; -- 原始列,可以分区剪枝
-- 验证分区剪枝
EXPLAIN PARTITIONS
SELECT * FROM orders
WHERE create_time BETWEEN '2024-01-01' AND '2024-06-30';
-- 查看partitions列,应仅显示目标分区
3.2 慢查询优化
sql
-- 使用覆盖索引优化
SELECT id, order_no, user_id, amount, create_time
FROM orders
WHERE user_id = ?
AND create_time > ?
ORDER BY create_time DESC
LIMIT 10;
-- 避免 SELECT *,减少回表查询
-- 分区表中为非分区键创建全局索引(MySQL 8.0.16+)
CREATE INDEX idx_amount ON orders (amount) GLOBAL;
3.3 热点数据问题
场景分析:
订单系统按user_id分片,发现Top卖家(如seller_id=1)订单占总量90%
原因分析:
- 大买家集中在少数用户(大客户)
- 某些平台大卖家订单量巨大
解决方案:
1. 改用user_id作为分片键(用户分布相对均匀)
2. 对Top卖家特殊处理:
- 方案1:单独为Top卖家创建独立分片
- 方案2:按user_id+order_id联合分片
- 方案3:订单表按seller_id单独分片
3. 数据迁移:
- 将Top卖家数据迁移到独立分片
- 平衡各分片数据量和负载
九、分库分表选型决策树
1. 是否需要分库分表判断标准
当满足以下任意2个条件时,应启动分库分表评估:
| 评估维度 | 判断标准 |
|---|---|
| 数据量 | 单表数据量>1500万行 |
| 并发量 | 核心业务表QPS>5000 |
| 存储成本 | 存储成本占比超IT预算25% |
| 性能瓶颈 | 优化后仍无法满足SLA要求 |
| 合规要求 | 监管要求必须物理隔离 |
2. 技术方案选型
开始评估
│
├─ 是否为微服务架构?
│ ├─ 是 → ShardingSphere-JDBC/Sidecar (推荐)
│ │
│ └─ 否 → 是否有专职中间件团队?
│ ├─ 是 → MyCat
│ └─ 否 → 考虑ShardingSphere轻量级治理模式
│
├─ 是否有遗留系统需改造?
│ ├─ 是 → MyCat (零侵入)
│ └─ 否 → ShardingSphere (性能更好)
│
└─ 技术栈是否为Java?
├─ 否 → MyCat (语言无关)
└─ 是 → ShardingSphere (集成更方便)
3. 实施建议
3.1 小规模起步
第一阶段(小型系统):
- 目标数据量: <1亿
- 分片数量: 4库 × 16表 = 64张表
- 优先级: 功能正确性 > 性能
- 验证重点: 数据一致性、业务功能完整性
3.2 渐进扩容
第二阶段(中型系统):
- 目标数据量: 1亿~10亿
- 分片数量: 8库 × 64表 = 512张表
- 引入: 读写分离 + 分布式事务
- 验证重点: 性能指标、跨分片查询优化
3.3 生产级架构
第三阶段(大型系统):
- 目标数据量: >10亿
- 分片数量: 16库 × 256表 = 4096张表
- 引入: 全局表、分布式追踪、智能路由
- 验证重点: 高可用、容灾切换、数据迁移自动化
十、最佳实践总结
1. 分库分表Checklist
分片键选择:
- 选择高频查询字段(覆盖90%以上查询场景)
- 确保数据分布均匀(避免倾斜)
- 分片键值稳定不变(避免数据迁移)
- 分片键包含在主键中
架构设计:
- 考虑垂直拆分(按业务模块/字段频次)
- 设计全局ID生成方案(避免冲突)
- 规划跨分片查询处理(广播表/绑定表/冗余)
- 设计数据迁移和扩容方案
运维体系:
- 建立完善的监控体系(性能/分片/延迟)
- 准备回滚和应急方案
- 制定数据备份与恢复流程
- 建立自动化运维工具(DDL执行/数据迁移)
2. 分片数量规划公式
分片数量计算示例:
数据量维度:
总数据量预估: 100TB
单分片建议容量: 500GB
数据量分片数 = ceil(100TB / 500GB) = 200个分片
并发量维度:
预估并发量: 100,000 QPS
单分片处理能力: 2,000 QPS
并发量分片数 = ceil(100,000 / 2,000) = 50个分片
最终分片数 = max(数据量分片数, 并发量分片数) = 200个分片
3. 避坑指南
| 坑点 | 问题描述 | 解决方案 |
|---|---|---|
| 分片键选错 | 使用订单状态、性别等枚举值,导致严重倾斜 | 改用user_id、order_id等稳定字段 |
| 全局ID冲突 | 多个实例自增ID重复 | 使用雪花算法或数据库自增序列 |
| 跨分片查询未优化 | 全分片扫描导致性能下降 | 设计广播表、冗余字段、绑定表 |
| 分片规则频繁变更 | 扩容时全量数据迁移 | 使用一致性哈希或预分片方案 |
| 忽略事务一致性 | 跨库操作未考虑分布式事务 | 引入Seata/TCC等分布式事务方案 |
| 过度分片 | 数据量不大就分片,增加复杂度 | 先进行SQL优化、索引优化 |
十一、常见问题FAQ
Q1: 什么时候应该考虑分库分表?
A: 当满足以下任意2个条件时,应启动分库分表评估:
- 单表数据量>1500万行
- 核心业务表QPS>5000
- 存储成本占比超IT预算25%
- 监管要求必须物理隔离
- 现有架构扩容成本>新架构实施成本
Q2: 垂直分表和水平分表如何选择?
A:
- 垂直拆分:适用于表字段过多(>50列)或字段访问频次差异大(高频<5%、低频>95%)的场景
- 水平拆分:适用于单表数据量超大(>1000万)或并发极高的场景
- 推荐做法:先垂直分库(业务解耦),再对核心大表水平分表(解决数据量问题)
Q3: 分片键选择错误会有什么后果?
A:
- 数据倾斜:某个分片数据量远超其他分片,成为性能瓶颈
- 跨分片查询增加:无法通过分片键定位,需要全分片扫描
- 数据迁移困难:扩容时需要全量数据重算和迁移
- 性能无法提升:分片后性能改善不明显,反而增加复杂度
Q4: 如何处理跨分片分页?
A: 三种方案:
- 基于分片键分页(推荐):查询条件包含分片键,直接定位目标分片
- 全分片扫描分页:遍历所有分片后应用层归并,但性能较差
- 使用搜索引擎:对复杂查询场景,建议使用Elasticsearch等
Q5: 分库分表是否支持事务?
A: 取决于具体方案:
- MySQL分区表:支持ACID事务(跨分区事务由MySQL协调)
- ShardingSphere:支持XA、Seata、TCC等多种分布式事务模式
- MyCat:依赖外部XA,复杂场景需应用层补偿
总结
本文档详细介绍了MySQL 8.0在应对海量数据和高并发场景下的分库分表解决方案,包括:
- 核心概念:垂直拆分vs水平拆分、分片策略类型
- 技术方案:MySQL分区表、MyCat、ShardingSphere-JDBC
- 实战案例:电商订单系统垂直+水平混合拆分方案
- 关键挑战:分布式ID、跨分片查询、数据迁移、监控告警
- 最佳实践:分片键选择、架构设计、运维体系建设
核心建议:
- 分库分表是架构演进的结果,而不是起点
- 只有在单库单表真正遇到瓶颈时,才应该考虑分片方案
- 先进行SQL优化、索引优化、读写分离,再考虑分库分表
- 选择合适的技术方案(ShardingSphere推荐用于新系统,MyCat适合遗留系统改造)
通过本文档的全面指南,可以搭建一个可扩展、高可用、易维护的分布式数据库架构体系,有效支撑业务从千万级到亿级数据的持续增长。
相关文档:
- 如需了解读写分离方案,可参考《MyCat读写分离架构与实战指南.md》
- 如需了解数据备份恢复方案,可参考《基于OpenEuler24.03的XtraBackup备份脚本.md》
- 如需了解MySQL SSL加密方案,可参考《OpenEuler24.03_MySQL_SSL配置指南.md》