MySQL分库分表策略

MySQL分库分表策略

MySQL 是业务系统中最常见的数据库,但用好它并不容易。索引设计、SQL 优化、事务处理都是日常开发中需要关注的点。本文从实际场景出发,讲常见问题和解决思路。

一、为什么需要分库分表

#

1.1 单库瓶颈

瓶颈类型 具体表现 临界值(参考)
数据量 查询和索引维护变慢 5000万-1亿行
并发连接 连接数耗尽 1000-2000连接
写入性能 写入延迟增加 5000-10000 TPS
存储容量 磁盘空间不足 1-2TB

#

1.2 分库 vs 分表

分表:解决单表数据量过大问题

  • 同一数据库中,表拆分为多张结构相同的表
  • user → user_0, user_1, user_2

分库:解决单库性能瓶颈

  • 数据分布到多个数据库实例
  • 每个库可以独立部署在不同服务器

分库分表:同时解决容量和性能问题

二、拆分策略

#

2.1 垂直拆分

垂直分库:按业务模块拆分

数据库拆分前:
一个数据库包含:user, order, product, log...

垂直分库后:
用户库:user, user_profile, user_address
订单库:order, order_item, payment
商品库:product, category, inventory

垂直分表:按字段拆分

表拆分前:
user表:id, name, avatar, bio, settings, ...(100+字段)

垂直分表后:
user_basic:id, name, phone, email(常用字段)
user_detail:id, avatar, bio, settings(大字段/不常用)
user_ext:id, ext_json(扩展字段)

适用场景

  • 垂直分库:业务耦合度低,可以独立演进
  • 垂直分表:表字段过多,冷热数据分离

#

2.2 水平拆分

水平分表:同一库内分多张表

user表 → user_0, user_1, user_2, ... user_15

水平分库:数据分布到多个库

DB0: user_0, user_1, user_2, user_3
DB1: user_4, user_5, user_6, user_7
DB2: user_8, user_9, user_10, user_11
DB3: user_12, user_13, user_14, user_15

适用场景

  • 单表数据量超过5000万
  • 写入性能达到瓶颈

三、分片算法

#

3.1 哈希取模

// 根据userId分片
int shard = userId % 16; // 16个分片
String tableName = "user_" + shard;

优点

  • 数据分布均匀
  • 写入分散,避免热点

缺点

  • 扩容需要迁移数据(2倍扩容)
  • 无法范围查询

#

3.2 范围分片

// 按时间或ID范围
if (userId >= 0 && userId < 1000000) {
table = "user_0";
} else if (userId < 2000000) {
table = "user_1";
}

优点

  • 扩容简单,增加新分片即可
  • 支持范围查询

缺点

  • 可能造成数据倾斜(新数据集中在最后一个分片)
  • 热点问题

#

3.3 一致性哈希

// 使用TreeMap实现一致性哈希
TreeMap<Long, String> virtualNodes = new TreeMap<>();

// 每个物理节点创建160个虚拟节点
for (String node : physicalNodes) {
for (int i = 0; i < 160; i++) {
long hash = hash(node + "#" + i);
virtualNodes.put(hash, node);
}
}

// 查找数据所属节点
long hash = hash(key);
Map.Entry<Long, String> entry = virtualNodes.ceilingEntry(hash);
if (entry == null) {
entry = virtualNodes.firstEntry();
}
String node = entry.getValue();

优点

  • 扩容时只影响少量数据

缺点

  • 实现复杂
  • 虚拟节点增加内存开销

#

3.4 分片算法对比

算法 数据分布 扩容成本 范围查询 热点问题
哈希取模 均匀 不支持
范围分片 可能倾斜 支持 可能有
一致性哈希 较均匀 不支持

#

3.5 分片键选择

选择原则

  1. 高频查询条件:大部分查询都能命中分片键
  2. 数据均匀:避免数据倾斜
  3. 业务无关:尽量不使用业务含义强的字段

常见选择

  • 用户ID
  • 订单ID(雪花算法)
  • 租户ID(SaaS系统)

四、ShardingSphere实践

#

4.1 引入依赖

<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-jdbc-core</artifactId>
<version>5.4.0</version>
</dependency>

#

4.2 配置分库分表

# application-sharding.yml
dataSources:
ds_0:
url: jdbc:mysql://localhost:3306/db0
username: root
password: password
ds_1:
url: jdbc:mysql://localhost:3306/db1
username: root
password: password

rules:
- !SHARDING
tables:
user:
actualDataNodes: ds_${0..1}.user_${0..15}
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: user_table_hash
databaseStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: user_database_hash
shardingAlgorithms:
user_table_hash:
type: INLINE
props:
algorithm-expression: user_${user_id % 16}
user_database_hash:
type: INLINE
props:
algorithm-expression: ds_${user_id % 2}
keyGenerators:
snowflake:
type: SNOWFLAKE

#

4.3 代码中使用

@Service
public class UserService {

@Autowired
private UserMapper userMapper;

public User getUser(Long userId) {
// ShardingSphere自动路由到对应分片
return userMapper.selectById(userId);
}

public void createUser(User user) {
// 使用雪花算法生成分布式ID
user.setUserId(IdWorker.getId());
userMapper.insert(user);
}
}

#

4.4 绑定表

具有关联关系的表使用相同分片策略:

bindingTables:
- order,order_item

这样order和order_item会路由到相同的分片,避免跨库JOIN。

五、分库分表后的问题

#

5.1 分布式主键

方案一:雪花算法

public class SnowflakeIdWorker {

private long workerId; // 机器ID
private long datacenterId; // 数据中心ID
private long sequence = 0; // 序列号

public synchronized long nextId() {
long timestamp = System.currentTimeMillis();

if (timestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards");
}

if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & 4095; // 12位序列号
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}

lastTimestamp = timestamp;

return ((timestamp - EPOCH) << 22)
| (datacenterId << 17)
| (workerId << 12)
| sequence;
}
}

方案二:号段模式

-- 发号器表
CREATE TABLE id_generator (
biz_tag VARCHAR(32) PRIMARY KEY,
max_id BIGINT NOT NULL,
step INT NOT NULL,
description VARCHAR(255)
);

-- 每次从数据库取一段ID缓存在内存中

#

5.2 跨分片查询

分页查询问题

-- 原SQL
SELECT * FROM user ORDER BY create_time LIMIT 100000, 10;

分片后需要:

  1. 在每个分片执行LIMIT 100000, 10
  2. 汇总结果排序
  3. 取最终的10条

优化方案

  • 限制深分页
  • 使用游标分页(每次带查询条件)
  • 建立全局索引表

#

5.3 跨分片JOIN

问题:分片键不同的表JOIN需要跨库查询

解决方案

  1. 绑定表:关联表使用相同分片策略
  2. 字段冗余:在需要JOIN的表中冗余字段
  3. 全局表:小表在每个分片都存一份
  4. 应用层组装:分别查询后在代码中JOIN

#

5.4 事务一致性

问题:跨分片事务无法使用本地事务

解决方案

  1. 最终一致性:使用消息队列
  2. Seata分布式事务
  3. Saga模式:补偿事务
  4. 避免跨分片事务:通过业务设计规避
// Seata分布式事务示例
@GlobalTransactional
public void createOrder(Order order) {
// 操作订单库
orderMapper.insert(order);

// 操作库存库
inventoryService.deduct(order.getProductId(), order.getQuantity());

// 操作用户库
userService.deductBalance(order.getUserId(), order.getAmount());
}

#

5.5 数据迁移

平滑迁移方案

  1. 双写阶段:应用同时写入旧库和新分片
  2. 数据同步:使用 Canal 等工具同步历史数据
  3. 数据校验:对比旧库和新分片数据
  4. 切换读流量:先切换部分读流量到新分片
  5. 切换写流量:确认无误后切写流量
  6. 下线旧库:观察一段时间,下线旧库

六、分库分表示例设计

#

6.1 电商系统分片设计

用户模块(分库分表):
DB: user_0, user_1
Table: user_0 ~ user_15
分片键:user_id

订单模块(分库分表):
DB: order_0, order_1
Table: order_0 ~ order_15
分片键:user_id(按买家)或 order_id

商品模块(全局表):
每个库都有完整的商品数据
Table: product

库存模块(分库):
DB: inventory_0, inventory_1
分片键:product_id

#

6.2 订单表分片策略

# 订单表:按user_id分片(方便查用户订单)
# 全局索引表:按order_id → user_id 映射(方便查单号)

order:
actualDataNodes: ds_${0..1}.order_${0..15}
tableStrategy:
standard:
shardingColumn: user_id
shardingAlgorithmName: order_hash

order_index:
actualDataNodes: ds_${0..1}.order_index_${0..15}
tableStrategy:
standard:
shardingColumn: order_id
shardingAlgorithmName: order_index_hash

七、避免过度拆分

#

7.1 拆分前的优化

在拆分前,先尝试:

  1. 索引优化:确保查询都有合适的索引
  2. SQL优化:避免全表扫描、深分页
  3. 读写分离:主从复制分担读压力
  4. 缓存:Redis缓存热点数据
  5. 归档:历史数据归档到历史库

#

7.2 拆分的时机

满足以下条件再考虑拆分:

  • 单表数据量 > 5000万
  • 单库QPS > 5000
  • 读写分离后仍无法满足
  • 优化空间已经很小

八、总结

拆分方式 解决什么问题 复杂度
垂直分库 业务耦合、连接数
垂直分表 字段过多、冷热数据
水平分表 单表数据量
水平分库 单库性能瓶颈
问题 解决方案
分布式ID 雪花算法、号段模式
跨分片查询 限制分页、全局索引
跨分片JOIN 绑定表、字段冗余
事务一致性 Seata、消息队列
数据迁移 双写+同步+校验

分库分表是”最后的手段”。在实施前,充分评估业务需求和拆分成本,选择合适的拆分策略和中间件,做好平滑迁移方案。

核心要点

  1. 索引设计的原则:最左前缀原则、避免索引列参与计算、选择合适的索引类型

  2. SQL 优化的方法:使用 EXPLAIN 分析执行计划、避免 SELECT *、优化 JOIN

  3. 事务隔离级别的选择:根据业务需求选择合适的级别

  4. 常见的锁问题:行锁、表锁、死锁的处理

总结

MySQL 优化是一个持续的过程,需要结合业务特点和数据量来调整。在实际项目中,定期分析慢查询日志、优化索引、调整配置参数,都能提升数据库性能。


   转载规则


《MySQL分库分表策略》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录