# 分库分表策略

> 选择合适的分片策略是分库分表的核心，直接影响系统性能和可扩展性。

## 垂直拆分 ⭐⭐⭐⭐

### 垂直分库

**定义**：按业务模块将表分散到不同的数据库。

```
原来：单库包含所有表
┌──────────────────────────────┐
│        Main Database         │
│  ┌──────┬──────┬──────────┐  │
│  │ User │ Order│ Product  │  │
│  └──────┴──────┴──────────┘  │
└──────────────────────────────┘

垂直分库后：
┌──────────┐  ┌──────────┐  ┌──────────┐
│ User  DB │  │ Order DB │  │Product DB│
│  User    │  │  Order   │  │ Product  │
└──────────┘  └──────────┘  └──────────┘
```

**优点**：

* 业务解耦，符合微服务架构
* 突破单库连接数限制
* 提升并发能力

**缺点**：

* 跨库 JOIN 困难
* 分布式事务问题

### 垂直分表

**定义**：将一张宽表拆分为多张表。

```
原来：user 表有 50 个字段
┌───────────────────────────────────────────────────┐
│ id, name, age, email, phone, avatar, address, ... │
└───────────────────────────────────────────────────┘

垂直分表后：
┌──────────────────────┐     ┌──────────────────────┐
│ user_basic           │     │ user_detail          │
│ id, name, age, email │     │ id, avatar, address  │
└──────────────────────┘     └──────────────────────┘
```

**优点**：

* 冷热数据分离，提升缓存命中率
* 减少 I/O，提升查询性能
* 核心字段更易维护

**缺点**：

* 需要额外 JOIN

***

## 水平拆分 ⭐⭐⭐⭐⭐

### 水平分库

**定义**：将同一张表的数据分散到多个库。

```
原来：单库 user 表 1 亿数据
┌──────────────────────┐
│   DB: user_db        │
│   Table: user        │
│   (100,000,000 rows) │
└──────────────────────┘

水平分库后：4 个库，每个库 2500 万数据
┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐
│ db_0   │  │ db_1   │  │ db_2   │  │ db_3   │
│ user   │  │ user   │  │ user   │  │ user   │
│ 2500万 │  │ 2500万 │  │ 2500万 │  │ 2500万 │
└────────┘  └────────┘  └────────┘  └────────┘
```

**优点**：

* 突破单库连接数和 QPS 限制
* 提升并发写入能力

**缺点**：

* 跨库 JOIN 和事务复杂
* 需要分片路由

### 水平分表

**定义**：将同一张表的数据分散到多个表。

```
原来：单表 user 5000 万数据
┌──────────────────┐
│ user (50M rows)  │
└──────────────────┘

水平分表后：4 张表，每张表 1250 万数据
┌───────┐  ┌───────┐  ┌───────┐  ┌───────┐
│user_0 │  │user_1 │  │user_2 │  │user_3 │
│1250万 │  │1250万 │  │1250万 │  │1250万 │
└───────┘  └───────┘  └───────┘  └───────┘
```

**优点**：

* 突破单表性能瓶颈
* 减少单表索引深度，提升查询性能

**缺点**：

* 跨表 JOIN 困难
* 需要处理分片路由

***

## 分片算法 ⭐⭐⭐⭐⭐

### 1. 取模算法

**公式**：`shard_id = user_id % shard_count`

```java
// 4 个分片
int shardId = userId % 4;
```

**优点**：

* 简单，数据分布均匀
* 查询性能好（精确定位分片）

**缺点**：

* **扩容困难**：增加分片需要迁移大量数据
* 例如：4 个分片扩展到 5 个，80% 数据需要迁移

### 2. 范围算法

**规则**：按 ID 范围划分分片。

```
user_id: 1-2500万     → db_0
user_id: 2500万-5000万 → db_1
user_id: 5000万-7500万 → db_2
user_id: 7500万-1亿    → db_3
```

**优点**：

* 扩容简单：只需增加新的范围分片
* 适合范围查询：`WHERE user_id BETWEEN 1000 AND 5000`

**缺点**：

* **数据倾斜**：新用户集中在最新分片，导致热点
* 历史分片可能空闲

**优化**：动态范围调整或混合算法

### 3. 一致性哈希

**原理**：使用哈希环，减少扩容时的数据迁移。

```
哈希环：0 ~ 2^32-1
节点分布在环上，数据顺时针找最近节点

扩容时只需迁移相邻节点的部分数据
```

**优点**：

* 扩容时只需迁移 1/N 数据（N 为节点数）
* 数据分布相对均匀

**缺点**：

* 实现复杂
* 需要虚拟节点防止数据倾斜

### 算法对比

| 算法        | 数据分布 | 扩容难度 | 适用场景           |
| --------- | ---- | ---- | -------------- |
| **取模**    | 均匀   | 困难   | 分片数固定，不需扩容     |
| **范围**    | 可能倾斜 | 简单   | 需要范围查询，数据增长可预测 |
| **一致性哈希** | 均匀   | 较简单  | 需要频繁扩容         |

***

## 分片键选择 ⭐⭐⭐⭐⭐

### 选择原则

1. **数据均匀分布**：避免热点
2. **查询条件包含分片键**：精确路由，避免广播
3. **业务相关性强**：如用户 ID、订单 ID
4. **避免跨分片**：相关数据在同一分片

### 常见分片键

| 场景      | 推荐分片键         | 理由          |
| ------- | ------------- | ----------- |
| **用户表** | user\_id      | 用户维度查询占大多数  |
| **订单表** | user\_id      | 按用户查询订单最常见  |
| **商品表** | shop\_id      | 按店铺查询商品     |
| **日志表** | 时间 + user\_id | 范围查询 + 用户维度 |

### 错误案例

**错误1**：选择非查询条件的字段

```sql
-- 分片键：region（地区）
-- 但查询通常是：WHERE user_id = ?
-- 结果：每次查询需要广播到所有分片
```

**错误2**：选择分布不均的字段

```sql
-- 分片键：status（订单状态）
-- 大部分订单状态为"已完成"
-- 结果：数据严重倾斜
```

***

## 跨分片查询 ⭐⭐⭐⭐⭐

### 问题

分库分表后，跨分片的 JOIN、聚合、排序都变得复杂。

**场景1：跨分片 JOIN**

```sql
-- 原来：单库可以直接 JOIN
SELECT u.name, o.order_no
FROM user u JOIN order o ON u.id = o.user_id
WHERE u.id = 123;

-- 分库后：user 和 order 在不同库，无法直接 JOIN
```

**场景2：全局排序**

```sql
-- 查询 Top 100 用户（按注册时间）
-- 需要查询所有分片，然后归并排序
```

### 解决方案

**方案1：冗余数据（推荐）**

```sql
-- order 表冗余 user_name 字段
-- 避免 JOIN
SELECT user_name, order_no FROM order WHERE user_id = 123;
```

**方案2：应用层 JOIN**

```java
// 1. 查询 user
User user = userDao.selectById(123); // db_3

// 2. 查询 order（带上 user_name）
List<Order> orders = orderDao.selectByUserId(123); // db_1

// 3. 应用层拼接
return buildUserOrderVO(user, orders);
```

**方案3：全局表**

```
将小表（如字典表）在每个分片都存一份
```

**方案4：ES 聚合**

```
将需要聚合的数据同步到 Elasticsearch
复杂查询走 ES，简单查询走 MySQL
```

***

## 扩容策略 ⭐⭐⭐⭐

### 扩容挑战

**取模算法扩容**：

```
4 个分片 → 8 个分片
user_id = 10: 原来在 db_2 (10 % 4 = 2)
              现在在 db_2 (10 % 8 = 2)  ✅ 不变

user_id = 9: 原来在 db_1 (9 % 4 = 1)
             现在在 db_1 (9 % 8 = 1)  ✅ 不变

user_id = 8: 原来在 db_0 (8 % 4 = 0)
             现在在 db_0 (8 % 8 = 0)  ✅ 不变

user_id = 7: 原来在 db_3 (7 % 4 = 3)
             现在在 db_7 (7 % 8 = 7)  ❌ 需要迁移
```

**结论**：约 50% 数据需要迁移

### 解决方案

**方案1：翻倍扩容**

```
2 → 4 → 8 → 16 ...
减少数据迁移比例
```

**方案2：逻辑分片 + 物理分片**

```
逻辑分片：1024 个（固定）
物理分片：4 个（可扩展）

映射：逻辑分片 % 物理分片
0-255   → db_0
256-511 → db_1
512-767 → db_2
768-1023 → db_3

扩容时：调整映射关系，减少迁移
```

**方案3：在线迁移工具**

```
使用 DTS（Data Transmission Service）等工具
双写 + 数据同步 + 校验
```

***

## 面试要点 ⭐⭐⭐⭐⭐

**Q1: 垂直分库和水平分库的区别？**

* 垂直：按业务拆分，解决业务耦合问题
* 水平：按数据量拆分，解决性能和存储问题

**Q2: 常见的分片算法有哪些？**

* 取模：均匀分布，但扩容困难
* 范围：扩容简单，但可能数据倾斜
* 一致性哈希：扩容时迁移数据少

**Q3: 如何选择分片键？**

* 数据均匀分布
* 查询条件包含分片键
* 避免跨分片查询

**Q4: 分库分表后如何处理 JOIN？**

* 数据冗余（推荐）
* 应用层 JOIN
* 全局表
* 使用 ES 等搜索引擎

**Q5: 取模算法扩容为什么困难？**

* 分片数变化后，大部分数据的分片位置都会变
* 例如 4 → 5，80% 数据需要迁移

**Q6: 如何减少扩容时的数据迁移？**

* 翻倍扩容
* 逻辑分片 + 物理分片
* 一致性哈希

**Q7: 分库分表后如何生成全局唯一 ID？**

* 雪花算法（推荐）
* 数据库号段
* Redis INCR
* UUID（无序，不推荐）

***

## 参考资料

1. **书籍推荐**：《大型网站技术架构》、《亿级流量网站架构核心技术》
2. **开源项目**：[ShardingSphere](https://shardingsphere.apache.org/)
3. **雪花算法**：[Twitter Snowflake](https://github.com/twitter-archive/snowflake)
