6.3 分库分表:数据爆炸时的计划生育政策
背景故事 :
当你的数据库变成"超生游击队"(单表数据超过千万级),查询慢得像挤地铁,这时候就需要"分库分表"------用"计划生育政策"给数据人口做精准管控!
6.3.1 什么是分库分表?
- 分库:把用户表、订单表等拆到不同数据库,就像把"超大城市"拆成"省-市-区"三级行政区
- 分表 :把单表拆成多张物理表,例如用户表按
user_id
拆成user_0
~user_9
,就像把"万人学校"拆成10个"千人分校"
6.3.2 实现方式 & 代码示例
场景 :电商系统用户表数据爆炸,需按user_id
分库分表
方案1:数据库分库(按用户ID哈希)
java
// 配置多个数据源(假设分3个库)
@Configuration
public class DataSourceConfig {
@Bean
public DataSource userDataSource() {
return new HikariDataSource(
Map.of("jdbcUrl", "jdbc:h2:mem:user_db",
"username", "sa",
"password", ""));
}
@Bean
public DataSource orderDataSource() {
return new HikariDataSource(
Map.of("jdbcUrl", "jdbc:h2:mem:order_db",
"username", "sa",
"password", ""));
}
}
// 分库策略:根据user_id选择数据源
public class DynamicDataSource {
private static final Map<Object, Object> DATA_SOURCES =
new HashMap<>();
static {
DATA_SOURCES.put("user", userDataSource());
DATA_SOURCES.put("order", orderDataSource());
}
public DataSource determineTargetDataSource() {
// 假设当前操作是用户表,返回对应库
return DATA_SOURCES.get("user");
}
}
方案2:分表(按时间范围)
java
// MyBatis分表插件示例
public class TimeBasedShardingAlgorithm
implements ShardingAlgorithm<Date> {
@Override
public String doSharding(
Collection<String> availableTargetNames,
ShardingValue<Date> shardingValue) {
Date date = shardingValue.getValue();
Calendar cal = Calendar.getInstance();
cal.setTime(date);
int month = cal.get(Calendar.MONTH);
return String.format("order_%d", month);
}
}
方案3:混合策略(用户ID哈希+时间分片)
java
// 用户表按ID哈希分库,订单表按时间分表
public class HybridShardingAlgorithm
implements ShardingAlgorithm<Long> {
@Override
public String doSharding(
Collection<String> availableTargetNames,
ShardingValue<Long> shardingValue) {
long userId = shardingValue.getValue();
int shard = (int) (userId % 3); // 分3个库
return String.format("user_%d", shard);
}
}
6.3.3 常见面试题 & 答案
Q1:分库分表的优缺点是什么?
- 优点 :
- 解决单库单表性能瓶颈(类比"分流交通")
- 数据分布均匀,避免热点(比如用户ID按哈希分库)
- 缺点 :
- 跨库JOIN操作困难(需要应用层处理)
- 分布式事务复杂度上升(需用TCC/Seata等方案)
Q2:常见的分表策略有哪些?如何选择?
- 哈希分表 :
用户表按userId % N
,适合数据均匀分布 - 范围分表 :
订单表按createTime分表
,适合时间序列数据 - 字段取模 :
商品表按商品类别分表
,业务场景明确时使用
Q3:分库分表后如何生成唯一主键?
-
方案对比 :
| 方案 | 适用场景 | 示例代码 |
|-----------|---------|-----------------------------------------------------------------------------|-------------------|-------------|
| UUID | 无序但足够唯一 |String uuid = UUID.randomUUID().toString();
|
| Snowflake | 高并发有序ID |java long id = IdWorker.nextId();
|
| 数据库自增ID | 单表场景 |@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
|
| 分库分表ID混合 | 复杂分布式场景 | `long dbId = 1; long tableId = 2; long sequence = 100; id = (dbId << 22) | (tableId << 12) | sequence;` |
Q4:分库分表后如何处理跨表JOIN查询?
- 解决方案 :
- 应用层合并:分步查询后在代码中关联(适合小数据量)
- 冗余字段:在订单表中冗余用户名称字段(避免JOIN)
- ES搜索引擎:通过ES实现跨库检索(适合复杂查询)
Q5:分库分表后如何保证事务一致性?
- 方案选择 :
-
最终一致性:消息队列+补偿机制(适合低频关键操作)
-
分布式事务框架 :
java// 使用Seata实现分布式事务 @GlobalTransactional public void placeOrder() { // 操作用户库 userMapper.updateBalance(userId); // 操作订单库 orderMapper.insert(order); }
-
Q6:分库分表后如何处理数据倾斜问题?
- 解决办法 :
- 哈希算法优化 :避免简单取模导致的热点(比如用
userId.hashCode() % N
) - 动态扩容:支持水平扩展新分片(比如从3个分库扩容到5个)
- 数据迁移:定期将热点数据迁移到新分片
- 哈希算法优化 :避免简单取模导致的热点(比如用
6.3.4 分库分表的致命伤 & 解决方案
问题:跨库统计用户总消费金额
sql
-- 原SQL(无法直接执行)
SELECT SUM(amount)
FROM orders
JOIN users ON orders.user_id = users.id
WHERE users.age > 25;
解决方案:
- 应用层聚合:
java
// 分别查询各分库数据再汇总
List<BigDecimal> amounts = new ArrayList<>();
for (DataSource ds : allDataSources) {
String sql = "SELECT SUM(amount) FROM orders WHERE ...";
amounts.add(ds.query(sql));
}
BigDecimal total = amounts.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
- ES索引辅助:
json
// 在ES中建立用户+订单联合索引
{
"mappings": {
"properties": {
"user_id": {"type": "keyword"},
"total_amount": {"type": "double"}
}
}
}
课程彩蛋:
- 分库分表工具推荐:ShardingSphere(阿里开源方案)
- 避坑指南 :分库后不要在WHERE子句中使用
user_id IN (1,2,3)
,会导致全库扫描
通过以上内容,面试官可能会问:"如果让你设计一个亿级用户系统的分库分表方案,你会怎么设计?"------现在你已经准备好了!