1. 数据分区
分区的主要目的是将一个大表在物理上分割成多个更小、更易管理的部分(称为分区),但在逻辑上仍然表现为一个完整的表。
- 工作原理:当对一个表进行查询时,如果查询条件包含了分区键(例如,按日期分区),数据库优化器可以执行“分区剪枝”,只扫描相关的分区,而忽略其他不相关的分区,大大提高了查询性能。
- 常见策略:
- 范围分区:按某个值的范围分区,如按日期
PARTITION p202401 VALUES LESS THAN ('2024-02-01')。 - 列表分区:按某个值的列表分区,如按地区
PARTITION p_east VALUES IN ('Beijing', 'Shanghai')。 - 哈希分区:通过对分区键进行哈希计算,将数据均匀分布到各个分区中。
- 范围分区:按某个值的范围分区,如按日期
2. 数据分片
分片是真正的分布式方案。它的核心思想是将一个巨大的数据集水平切分,并将不同的数据块(分片)存储在不同的数据库节点上。
- 工作原理:每个分片都是整个数据集的一个子集,所有分片一起构成完整的逻辑数据集。应用需要根据分片键 通过一个路由层(可以是独立的中间件,或内置于数据库客户端)来确定数据应该存储在哪个或从哪个分片节点上读取。
- 常见策略:
- 基于哈希:最常用的方式,能保证数据相对均匀地分布到各个节点。
- 基于地理位置:将用户数据路由到离他们最近的物理数据中心的分片上。
3. 分库分表
“分库分表”这个词本身是一个统称,在实践中,细分为以下三种主要手段:
3.1. 只分表不分库
将所有拆分后的子表都存放在同一个数据库实例中。
- 例子:将
user表拆分为user_0,user_1, ...user_9,但这10张子表仍然在同一个MySQL实例里。 - 与分区的关系:这种方式的“分表”在效果上非常类似于“分区”。目标都是解决单表过大问题,并且数据都在同一台服务器上。区别在于:
- 分表 通常由应用层或中间件管理路由,数据库层面看到的完全是独立的表。
- 分区 由数据库内核自身支持,在数据库层面仍然是一个逻辑表。
3.2. 只分库不分表
设置了多个数据库实例,但每个实例中的表结构并没有被拆分。这通常用于读写分离或根据业务模块进行隔离。
- 例子:将用户相关的数据放在
user_db,订单相关的数据放在order_db。这两个数据库可能在不同的服务器上。user_db中依然有一张完整的user表。 - 与分片的关系:这已经具备了数据分片的雏形,因为数据确实分布到了不同的节点上。但它不是水平切分,而是基于业务逻辑的垂直切分。
3.3. 既分库又分表(真正的水平分片)
这是最经典、也是最复杂的“分库分表”方案。它先进行“分库”(将数据分布到多个物理节点),在每个库内再进行“分表”(将单个逻辑表拆成多个物理表)。
- 例子:将
user表的数据,通过user_id哈希后,分布到2个数据库(db_0,db_1)中,每个数据库内部又包含10张子表(如db_0.user_0...db_0.user_9)。 - 与分片的关系:这就是完整的数据分片实践。它同时实现了数据的水平分布和单表数据的切分,旨在彻底解决海量数据和高并发下的单机瓶颈问题。
4. 具体场景
假设我们有一个电商平台,其中有一张核心的订单表(orders),随着业务发展,数据量达到了10亿级别,单机MySQL已经无法承受。
场景1:使用数据分区
方案:我们继续使用单台MySQL服务器,但对orders表按月份进行范围分区。
CREATE TABLE orders (
id BIGINT AUTO_INCREMENT,
user_id BIGINT,
amount DECIMAL(10,2),
order_date DATE,
-- ... 其他字段
PRIMARY KEY (id, order_date) -- 分区键必须包含在主键中
) PARTITION BY RANGE (YEAR(order_date)*100 + MONTH(order_date)) (
PARTITION p202401 VALUES LESS THAN (202402),
PARTITION p202402 VALUES LESS THAN (202403),
PARTITION p202403 VALUES LESS THAN (202404),
-- ... 后续分区
PARTITION p_max VALUES LESS THAN MAXVALUE
);
数据分布:
-
p202401分区:存储2024年1月的所有订单 -
p202402分区:存储2024年2月的所有订单 - ...
所有分区都在同一台MySQL服务器的同一个数据库中。
查询示例:
-- 这个查询很高效,数据库只会扫描p202401分区(分区剪枝)
SELECT * FROM orders WHERE order_date BETWEEN '2024-01-01' AND '2024-01-31';
-- 这个查询性能较差,需要扫描所有分区
SELECT * FROM orders WHERE user_id = 12345;
运维操作:
-- 可以快速删除3年前的老数据,只需删除对应分区
ALTER TABLE orders DROP PARTITION p202101;
优点:管理方便,对历史数据清理和基于时间的查询性能提升明显。
缺点:所有数据仍在一台服务器上,磁盘I/O、CPU、内存的瓶颈依然存在。
场景2:使用数据分片(分库分表)
由于单机性能达到瓶颈,决定采用既分库又分表的方案。
- 2个数据库(db_0, db_1),分布在不同的服务器上
- 每个库中4张订单表(orders_0, orders_1, orders_2, orders_3)
- 使用用户ID(user_id) 作为分片键
分片策略:分片位置 = user_id % (2个库 * 4张表) = user_id % 8
- 计算结果0-3:落在 db_0 库
- 计算结果4-7:落在 db_1 库
- 具体表名由
user_id % 4决定
数据分布:
| user_id | 计算(user_id % 8) | 实际位置 |
|---|---|---|
| 101 | 101 % 8 = 5 | db_1.orders_1 (因为5 % 4 = 1) |
| 202 | 202 % 8 = 2 | db_0.orders_2 |
| 305 | 305 % 8 = 1 | db_0.orders_1 |
应用层查询(需要通过中间件如ShardingSphere或自研路由):
-- 应用传入user_id=101,中间件自动路由到db_1.orders_1表
SELECT * FROM orders WHERE user_id = 101;
-- 如果要查询所有用户的订单(跨分片查询),需要查询所有8张表然后合并结果
SELECT * FROM orders WHERE amount > 1000;
架构图示:
逻辑表: orders
↙ ↘
db_0 (服务器1) db_1 (服务器2)
orders_0 orders_0
orders_1 orders_1
orders_2 orders_2
orders_3 orders_3
优点:真正实现了水平扩展,数据量和访问压力分散到多台服务器。
缺点:系统复杂性极高,需要处理分布式事务、跨分片查询、数据迁移等。
场景3:分区 + 分片组合使用
在实际生产环境中,可以组合使用这两种技术:
方案:在已经分库分表的基础上,在每个分片表内部再进行分区。
-- 在db_0.orders_0表中继续按时间分区
CREATE TABLE db_0.orders_0 (
id BIGINT AUTO_INCREMENT,
user_id BIGINT,
amount DECIMAL(10,2),
order_date DATE,
PRIMARY KEY (id, order_date)
) PARTITION BY RANGE (YEAR(order_date)*100 + MONTH(order_date)) (
PARTITION p202401 VALUES LESS THAN (202402),
PARTITION p202402 VALUES LESS THAN (202403),
-- ...
);
这样设计的优势:
- 分片解决了跨服务器的水平扩展问题
- 分区解决了单个分片内部的数据管理和时间范围查询优化问题
- 可以轻松地按时间清理单个分片中的历史数据
总结对比
| 操作 | 数据位置 | 解决的核心问题 | 适用场景 |
|---|---|---|---|
| 数据分区 | 单台服务器内 | 单表过大、数据管理、时间范围查询 | 数据量在单机容量内,但有明显的时间或范围特征 |
| 分库分表 | 多台服务器间 | 数据量超单机极限、高并发压力 | 海量数据、需要水平扩展的真正分布式场景 |
| 分区+分片 | 多台服务器,每台服务器内再分区 | 同时解决扩展性和单机数据管理问题 | 超大规模、需要精细化管理数据的生产环境 |
关键理解:
- 分区是"纵向"深化:在单机内部把数据整理得更好
- 分片是"横向"扩展:把数据分散到更多机器上
- 分库分表是分片思想在MySQL中的具体实现