MySQL-分库分表详解
一、背景与问题
随着业务规模扩大,单体MySQL数据库面临三大核心问题:
- 写性能瓶颈:单表数据量超过千万级时,写入效率急剧下降
- 读性能瓶颈:单表查询时,索引效率下降导致慢查询
- 数据量瓶颈:单实例存储容量受限,扩展性差
传统解决方案包括读写分离、主从复制、索引优化等,但这些方案在应对超大规模数据时存在本质限制。分库分表作为水平扩展的核心手段,通过将数据分散到多个数据库和表中,可有效解决上述问题,但同时也引入了新的挑战。
二、基本原理
1. 分库与分表的区别
分库:按业务维度划分数据库,如用户库、订单库、商品库等。每个库包含完整的业务表结构,但数据属于不同业务域。
分表:按数据维度划分表,如用户表拆分为user_001、user_002等。每个分表包含相同结构的数据,但数据按规则分布。
2. 分片策略
核心是分片键(Sharding Key)的选择,常见的分片算法包括:
- 哈希分片:通过哈希函数计算分片值,适合数据分布均匀的场景
- 范围分片:按主键范围划分,适合按时间或ID分页查询的场景
- 一致性哈希:平衡数据分布和扩展性,适合动态扩容的场景
3. 分库分表的架构
客户端 -> 分片中间件 -> 分库分表 -> 存储层分片中间件负责:
- 分片键解析
- 路由计算
- 读写分离
- 事务协调
三、环境准备
1. 系统要求
- MySQL 5.7+(支持分片中间件)
- 分片中间件(如ShardingSphere)
- 开发环境:Java 8+ / Python 3.8+
2. 分库分表配置
以ShardingSphere为例,配置文件如下:
spring:
shardingsphere:
rules:
sharding:
tables:
user:
actual-data-nodes: ds$->{0..1}.user_$->{0..1}
database-strategy:
standard:
sharding-column: user_id
sharding-Algorithm: user-database-inline
table-strategy:
standard:
sharding-column: user_id
sharding-Algorithm: user-table-inline
props:
sql-show: true3. 分片算法实现
// 哈希分片算法
public class HashShardingAlgorithm implements StandardShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> availableTargetNames, ShardingValue<Long> shardingValue) {
int hash = shardingValue.getValue() % 2; // 假设分2个库
return "ds" + hash;
}
}四、核心实现
1. 分库分表实现
// 分库分表策略配置
@Configuration
public class ShardingConfig {
@Bean
public ShardingSphereDataSource dataSource() {
ShardingSphereDataSource dataSource = ShardingSphereDataSourceBuilder.create();
// 分库策略
StandardShardingAlgorithm databaseAlgorithm = new HashShardingAlgorithm();
dataSource.getRuleConfig().getDatabaseShardingRule().setShardingColumn("user_id");
dataSource.getRuleConfig().getDatabaseShardingRule().setShardingAlgorithm(databaseAlgorithm);
// 分表策略
StandardShardingAlgorithm tableAlgorithm = new HashShardingAlgorithm();
dataSource.getRuleConfig().getTableShardingRule().setShardingColumn("user_id");
dataSource.getRuleConfig().getTableShardingRule().setShardingAlgorithm(tableAlgorithm);
return dataSource;
}
}2. 分片键选择
// 哈希分片算法实现
public class HashShardingAlgorithm implements StandardShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> availableTargetNames, ShardingValue<Long> shardingValue) {
int hash = shardingValue.getValue() % 2; // 假设分2个库
return "ds" + hash;
}
}3. 分库分表查询
-- 分库分表查询示例
SELECT * FROM user WHERE user_id = 123456;五、完整案例
1. 电商系统分库分表案例
业务场景:用户表user,预计10亿条数据,日均新增100万
分库分表方案:
- 分库:按用户ID的哈希值分2个库(ds0, ds1)
- 分表:按用户ID的哈希值分4个表(user_0, user_1, user_2, user_3)
配置文件:
spring:
shardingsphere:
rules:
sharding:
tables:
user:
actual-data-nodes: ds$->{0..1}.user_$->{0..3}
database-strategy:
standard:
sharding-column: user_id
sharding-Algorithm: user-database-inline
table-strategy:
standard:
sharding-column: user_id
sharding-Algorithm: user-table-inline分片算法实现:
// 分库算法
public class UserDatabaseShardingAlgorithm implements StandardShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> availableTargetNames, ShardingValue<Long> shardingValue) {
int hash = shardingValue.getValue() % 2;
return "ds" + hash;
}
}
// 分表算法
public class UserTableShardingAlgorithm implements StandardShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> availableTargetNames, ShardingValue<Long> shardingValue) {
int hash = shardingValue.getValue() % 4;
return "user_" + hash;
}
}六、源码解析
1. 分片算法执行流程
- 客户端发送SQL
- 分片中间件解析SQL,提取分片键
- 执行分片算法计算分片值
- 根据分片值路由到对应数据库/表
- 执行SQL并返回结果
2. 分片算法实现细节
// 哈希分片算法实现
public class HashShardingAlgorithm implements StandardShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> availableTargetNames, ShardingValue<Long> shardingValue) {
int hash = shardingValue.getValue().hashCode() % availableTargetNames.size();
return "ds" + hash;
}
}3. 分片键选择策略
// 分片键选择策略
public class ShardingKeySelector implements ShardingKeySelector {
@Override
public Collection<ShardingValue> getShardingValues(String logicTableName, String shardingColumn, Object value) {
return Collections.singletonList(new ShardingValue("user_id", value));
}
}七、进阶使用
1. 分库分表事务处理
// 分布式事务处理
@Transactional
public void transferMoney(Long fromUserId, Long toUserId, BigDecimal amount) {
// 查询fromUser
User fromUser = userRepository.findByUserId(fromUserId);
// 查询toUser
User toUser = userRepository.findByUserId(toUserId);
// 扣除fromUser金额
fromUser.setBalance(fromUser.getBalance().subtract(amount));
// 增加toUser金额
toUser.setBalance(toUser.getBalance().add(amount));
// 保存数据
userRepository.save(fromUser);
userRepository.save(toUser);
}2. 动态分片策略
// 动态分片策略实现
public class DynamicShardingAlgorithm implements StandardShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> availableTargetNames, ShardingValue<Long> shardingValue) {
int shardCount = 2; // 动态获取分片数
int hash = shardingValue.getValue() % shardCount;
return "ds" + hash;
}
}八、性能与工程实践
1. 性能优化策略
| 优化策略 | 描述 | 适用场景 |
|---|---|---|
| 分片键选择 | 选择分布均匀的字段 | 数据分布均匀 |
| 读写分离 | 分离读写流量 | 高并发场景 |
| 缓存优化 | 使用本地缓存减少数据库访问 | 频繁查询场景 |
| 索引优化 | 在分片键上建立索引 | 查询性能优化 |
2. 分库分表的挑战
- 数据分布不均:哈希冲突导致某些分片压力过大
- 跨分片查询:需要进行分片路由计算
- 事务一致性:分布式事务处理复杂
3. 安全风险
- 分片键泄露:分片键信息暴露可能导致数据定位
- 权限控制:需要为每个分片设置独立的访问控制
- 数据隔离:不同业务库需要严格隔离
九、常见问题与踩坑
1. 常见错误
| 错误场景 | 原因 | 解决方案 |
|---|---|---|
| 分片键选择不当 | 导致数据分布不均 | 选择分布均匀的字段 |
| 跨分片查询效率低 | 需要进行分片路由 | 使用分片中间件 |
| 分片键重复 | 哈希冲突 | 增加分片数量 |
2. 常见问题
- 分片键选择:避免使用业务关联强的字段
- 分片数量配置:建议初始配置为2-4个分片
- 数据迁移:需要考虑数据迁移策略
3. 典型问题
-- 错误示例:跨分片查询
SELECT * FROM user WHERE user_id IN (1, 2, 3);十、最佳实践
1. 推荐做法
- 分片键选择:优先选择业务无关的字段(如ID)
- 分片数量:建议初始配置为2-4个分片,按需扩展
- 分片中间件:使用成熟的中间件(如ShardingSphere)
- 数据监控:定期检查数据分布和分片负载
- 事务处理:使用分布式事务框架(如Seata)
2. 不推荐做法
- 分片键选择:避免使用业务关联强的字段
- 分库分表:不适用于小规模系统
- 数据迁移:避免频繁调整分片策略
十一、总结
分库分表是解决MySQL水平扩展的核心手段,但需要谨慎选择分片策略和分片键。在实际项目中,应根据业务需求选择合适的分片方式,同时注意处理分片带来的挑战。通过合理选择分片键、使用成熟的中间件、优化分片策略,可以有效提升数据库性能和扩展性。在实施过程中,需要持续监控数据分布和系统性能,及时调整分片策略以应对业务增长。