前言
单机 MySQL 的瓶颈如下:
关于分库、分表和读写分离的架构图如下所示;
1、分库分表分区
1.1、联系
| 技术 | 物理位置 | 逻辑视图 | 是否跨实例 |
|---|---|---|---|
| 分区(Partitioning) | 同一表、同一库、同一 MySQL 实例 | 仍是一个表 | 否 |
| 分表(Table Sharding) |
多个表(如 user_0, user_1),同一库
|
应用需知道多个表 | 否 |
| 分库(Database Sharding) |
多个数据库(如 db_0, db_1),可能跨 MySQL 实例
|
应用需路由到不同库 | 是 |
核心思想:
- 分区:数据库内部优化
- 分库分表:应用层或中间件实现的分布式架构
1.2、对比
| 能力 | 分区 | 分表 | 分库 |
|---|---|---|---|
| 是否跨实例 | ❌ | ❌ | ✅ |
| 应用是否感知 | ❌(透明) | ✅(需拼表名) | ✅(需路由) |
| 突破单机瓶颈 | ❌ | ❌(仍在单库) | ✅ |
| 支持跨分片查询 | ✅(自动裁剪) | ❌ | ❌(需中间件模拟) |
| 扩容难度 | 低 | 高 | 极高 |
| 典型工具 | MySQL 原生 | 应用层 | ShardingSphere, Vitess |
2、分区(Partitioning)
2.1、介绍
MySQL 原生支持将一个大表的数据物理拆分到多个“分区”中,但逻辑上仍是一个表,数据库内置能力。
开发人员在数据操作时仍然是对这个整体大表进行操作,之后由数据库底层内部去寻找对应的分区进行操作,这样在数据操作时可以只对特定分区操作以提高效率,存储时也可以将不同分区的物理文件分开存放。
2.2、核心原理
-
物理存储分离:每个分区是一个独立
.ibd文件 - 逻辑视图统一:应用仍操作一个表名
- 查询自动裁剪:优化器只扫描相关分区
2.3、常见分区类型
1.RANGE 分区(按范围)
如下所示:
-- 按年份分区 CREATE TABLE sales ( id BIGINT, sale_date DATE, amount DECIMAL(10,2) ) PARTITION BY RANGE (YEAR(sale_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 ); -- 查询自动裁剪 EXPLAIN SELECT * FROM sales WHERE sale_date = '2023-06-01'; -- 只扫描 p2023 分区
(2) LIST 分区(按枚举值)
如下所示:
-- 按地区分区
CREATE TABLE users (
id BIGINT,
region VARCHAR(10)
) PARTITION BY LIST COLUMNS(region) (
PARTITION p_north VALUES IN ('BJ', 'TJ'),
PARTITION p_south VALUES IN ('GZ', 'SZ'),
PARTITION p_west VALUES IN ('CD', 'XA')
);
(3) HASH 分区(均匀分布)
如下所示:
-- 按 user_id 均匀分布到 4 个分区 CREATE TABLE orders ( id BIGINT, user_id BIGINT ) PARTITION BY HASH(user_id) PARTITIONS 4;
(4) KEY 分区(类似 HASH,支持多列)
如下所示:
PARTITION BY KEY(user_id, order_type) PARTITIONS 8;
2.4、分区管理命令
-- 添加新分区 ALTER TABLE sales ADD PARTITION ( PARTITION p2025 VALUES LESS THAN (2026) ); -- 删除旧分区(秒级!) ALTER TABLE sales DROP PARTITION p2022; -- 重建分区(整理碎片) ALTER TABLE sales REBUILD PARTITION p2023;
✅ 优点
-
对应用透明:SQL 不用改,
SELECT * FROM logs WHERE create_time > '2024-01-01'自动只查p2024 -
提升查询性能:分区裁剪(Partition Pruning)
-
方便数据管理:
ALTER TABLE logs DROP PARTITION p2023;快速删除旧数据
❌ 缺点
-
仍在单机:无法突破单 MySQL 实例的 CPU/内存/磁盘瓶颈
-
分区数有限:通常建议 < 100 个分区
-
不支持所有引擎:仅 InnoDB、MyISAM 支持
📌 适用场景
-
单表过大(> 1000万行),但总数据量未超单机容量
-
按时间冷热分离(如日志、订单)
-
需要快速删除历史数据
3、分表(Table Sharding)
3.1、介绍
将一张大表拆成多张结构相同的表,应用层拆分。
如下所示:
3.2、使用原因
为什么需要分表?
-
InnoDB 单表建议 < 5000万行(B+树深度增加)
-
表锁/元数据锁竞争(DDL 操作阻塞)
如:
-
user_0, user_1, user_2, user_3
如何路由?
通常用 分片键(Shard Key) + 取模/哈希 决定数据存哪张表。
如下所示:
示例:按 user_id 分 4 张表
// Java 伪代码
public String getTableName(Long userId) {
int tableIndex = (int) (userId % 4);
return "user_" + tableIndex;
}
// 查询用户
String sql = "SELECT * FROM " + getTableName(userId) + " WHERE id = ?";
3.3、分片策略设计
1.分片键选择
| 字段 | 优点 | 缺点 |
|---|---|---|
| user_id | 用户数据聚集 | 热点用户问题 |
| order_id | 均匀分布 | 无法按用户查订单 |
| tenant_id | 多租户隔离 | 租户数据不均 |
最佳实践:选高频查询字段作为分片键
2.分片算法
// 取模(简单但扩容难) int tableIndex = userId % tableCount; // 一致性哈希(扩容友好) HashFunction hash = Hashing.murmur3_32(); int bucket = hash.hashLong(userId).asInt() & Integer.MAX_VALUE; int tableIndex = bucket % tableCount;
3.4、MyBatis + 分表
步骤 1:定义分表路由
@Component
public class TableShardingRouter {
private static final int TABLE_COUNT = 4;
public String getTableName(String baseName, Long shardKey) {
int index = (int) (shardKey % TABLE_COUNT);
return baseName + "_" + index;
}
}
步骤 2:MyBatis 动态表名
<!-- UserMapper.xml -->
<select id="selectById" resultType="User">
SELECT * FROM ${tableName} WHERE id = #{id}
</select>
步骤 3:Service 层调用
@Service
public class UserService {
@Autowired
private TableShardingRouter router;
@Autowired
private UserMapper userMapper;
public User getUser(Long userId) {
String tableName = router.getTableName("user", userId);
return userMapper.selectById(tableName, userId);
}
}
优点
-
突破单表性能瓶颈(InnoDB 单表建议 < 5000万行)
-
实现简单,无需中间件
缺点
-
应用强耦合:每个 SQL 都要拼表名;
-
无法跨表 JOIN / 聚合:select count(*) from user_* 需查 4 次再 sum;
-
扩容困难:从 4 表扩到 8 表,需迁移一半数据;
适用场景
-
单库能扛住,但单表太大
-
查询基本都带分片键(如 where user_id = ?)
4、分库(Database Sharding)
4.1、分库原因
为什么需要分库?
-
单机资源耗尽(CPU/内存/IO)
-
连接数瓶颈(
max_connections) -
多租户数据隔离需求
4.2、分库分表组合模式
模式 1:库内分表(推荐)
-
4 个库 × 每库 16 张表 = 64 分片
-
优点:减少数据库连接数
模式 2:仅分库
-
4 个库 × 每库 1 张表
-
优点:简化表结构
📊 计算公式:
总分片数 = 库数量 × 表数量;
分片ID = hash(shard_key) % 总分片数;
库名 = 分片ID / 表数量;表名 = 分片ID % 表数量
4.3、架构
如下所示:
是什么?
将数据分散到多个数据库实例(可能在不同机器),真正的分布式。
如:
-
db_0(IP: 10.0.0.1)
-
db_1(IP: 10.0.0.2)
通常分库 + 分表一起用,如:
-
db_0
.user_0, db_0.user_1 -
db_1
.user_0, db_1.user_1
架构图
Application
│
├── Router (ShardingSphere / MyCat / 自研)
│ │
│ ├── db_0 (10.0.0.1)
│ │ ├── user_0
│ │ └── user_1
│ │
│ └── db_1 (10.0.0.2)
│ ├── user_0
│ └── user_1
优点
-
水平扩展:突破单机资源限制(CPU/内存/连接数)
-
高可用:一个库挂了,其他库仍可用
-
隔离性:租户/业务线数据物理隔离
缺点
-
复杂度飙升:
-
跨库事务(需 Seata/TCC)
-
跨库 JOIN(需应用层聚合)
-
全局 ID(需 Snowflake/Leaf)
-
-
运维成本高:备份、监控、扩容都变复杂
适用场景
-
海量数据(> 1TB)
-
超高并发(> 1万 QPS)
-
多租户 SaaS 系统
5、最佳实践
1.不要过早分库分表→ 先优化 SQL、加索引、读写分离、缓存
2.分片键至关重要→ 选高频查询字段(如 user_id),避免热点
3.避免跨分片操作→ 设计时让关联数据落在同分片(如订单 & 订单项都按 user_id 分)
4.全局 ID 必须趋势递增→ 避免 UUID 导致 InnoDB 性能下降;
总结
结论:
分区是“治标”,分库分表是“治本”。90% 的系统,用好 分区 + 读写分离 + 缓存 就足够了;
只有真正遇到单机瓶颈,才考虑分库分表。
参考文章:













