问题
分库分表的策略有哪些?
答案
1. 核心概念
分库分表策略本质上是数据路由算法,用于决定一条数据应该存储在哪个库、哪张表中。选择合适的分片策略直接影响系统的查询性能、扩展性和数据均衡性。
2. 主要分片策略
2.1 范围分片(Range Sharding)
原理:按某个字段的值范围划分,如按ID、时间等
优点:
- 范围查询效率高
- 扩容时只需新增分片
- 数据分布直观,便于运维
缺点:
- 容易出现数据热点
- 分布可能不均匀
实现示例:
// 按订单ID范围分表
public class RangeShardingStrategy {
public String route(Long orderId) {
if (orderId < 10_000_000) {
return "order_0";
} else if (orderId < 20_000_000) {
return "order_1";
} else if (orderId < 30_000_000) {
return "order_2";
} else {
return "order_3";
}
}
}
// 按时间范围分表(常用于日志、订单等场景)
public class TimeRangeShardingStrategy {
public String route(Date createTime) {
String yearMonth = new SimpleDateFormat("yyyyMM").format(createTime);
return "order_" + yearMonth; // order_202501, order_202502...
}
}
适用场景:
- 订单表按月份归档
- 日志表按日期分表
- 有明确范围查询需求的业务
2.2 哈希取模分片(Hash Modulo)
原理:对分片键进行哈希运算后取模
优点:
- 数据分布均匀
- 实现简单
- 避免热点问题
缺点:
- 扩容困难(需要数据迁移)
- 范围查询需要访问所有分片
实现示例:
// 按用户ID哈希分库分表
public class HashModuloStrategy {
private static final int DB_COUNT = 4; // 4个数据库
private static final int TABLE_COUNT = 8; // 每个库8张表
// 分库路由
public String routeDatabase(Long userId) {
int dbIndex = Math.abs(userId.hashCode() % DB_COUNT);
return "db_" + dbIndex; // db_0, db_1, db_2, db_3
}
// 分表路由
public String routeTable(Long userId) {
int tableIndex = Math.abs(userId.hashCode() % TABLE_COUNT);
return "user_" + tableIndex; // user_0 ~ user_7
}
// 完整路由
public ShardingTarget route(Long userId) {
return new ShardingTarget(
routeDatabase(userId),
routeTable(userId)
);
}
}
适用场景:
- 数据分布要求均匀
- 主要是点查询(根据ID查询)
- 分片数量相对固定
2.3 一致性哈希(Consistent Hashing)
原理:使用哈希环,节点和数据都映射到环上,数据存储到顺时针最近的节点
优点:
- 扩容时只影响相邻节点
- 数据迁移量小
缺点:
- 实现复杂
- 可能出现数据倾斜(通过虚拟节点解决)
实现示例:
public class ConsistentHashStrategy {
private final TreeMap<Long, String> ring = new TreeMap<>();
private final int virtualNodes = 150; // 虚拟节点数
// 添加物理节点
public void addNode(String node) {
for (int i = 0; i < virtualNodes; i++) {
String virtualNode = node + "#" + i;
long hash = hash(virtualNode);
ring.put(hash, node);
}
}
// 路由到目标节点
public String route(Long userId) {
long hash = hash(String.valueOf(userId));
// 找到顺时针方向最近的节点
Map.Entry<Long, String> entry = ring.ceilingEntry(hash);
if (entry == null) {
entry = ring.firstEntry(); // 环形结构
}
return entry.getValue();
}
private long hash(String key) {
return MurmurHash.hash64(key); // 使用MurmurHash
}
}
适用场景:
- 需要频繁扩容的场景
- 分布式缓存(Redis集群)
- 节点动态变化的系统
2.4 地理位置分片(Geo Sharding)
原理:按地理位置(地区、城市)划分
实现示例:
public class GeoShardingStrategy {
private static final Map<String, String> REGION_DB_MAP = Map.of(
"华北", "db_north",
"华东", "db_east",
"华南", "db_south",
"西南", "db_southwest"
);
public String route(String region) {
return REGION_DB_MAP.getOrDefault(region, "db_default");
}
}
适用场景:
- 跨国/跨地区业务
- 就近访问,减少延迟
- 数据主权要求(如GDPR)
2.5 复合分片策略
原理:组合多种策略,先分库再分表
实现示例:
public class CompositeShardingStrategy {
private static final int DB_COUNT = 4;
private static final int TABLE_COUNT = 16;
public ShardingTarget route(Long userId) {
// 先按用户ID哈希分库
int dbIndex = Math.abs(userId.hashCode() % DB_COUNT);
// 再按用户ID取模分表
int tableIndex = (int) (userId % TABLE_COUNT);
return new ShardingTarget(
"db_" + dbIndex,
"user_" + tableIndex
);
}
}
3. 分片策略对比
| 策略 | 数据均匀性 | 扩容难度 | 范围查询 | 实现复杂度 | 典型应用 |
|---|---|---|---|---|---|
| 范围分片 | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | 订单归档、日志 |
| 哈希取模 | ⭐⭐⭐⭐⭐ | ⭐⭐ | ⭐ | ⭐⭐⭐ | 用户表、订单表 |
| 一致性哈希 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐ | ⭐⭐⭐⭐ | 缓存、动态扩容场景 |
| 地理分片 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ | 国际化业务 |
4. 分片键的选择原则
// 好的分片键特征:
// 1. 高基数(Cardinality):取值范围广,分布均匀
// 2. 查询常用:大部分查询都包含分片键
// 3. 不可变性:分片键值不应频繁更新
// 示例:订单表的分片键选择
public class OrderShardingKeyChoice {
// ❌ 不好的选择:订单状态(基数低,只有几个值)
String shardingKey1 = order.getStatus();
// ❌ 不好的选择:创建时间(范围查询多,但会造成热点)
Date shardingKey2 = order.getCreateTime();
// ✅ 推荐选择:订单ID(高基数、均匀分布、不可变)
Long shardingKey3 = order.getOrderId();
// ✅ 也可选择:用户ID(如果按用户查询订单是主要场景)
Long shardingKey4 = order.getUserId();
}
5. 实际应用建议
5.1 订单表分片策略
// 推荐:用户ID哈希 + 订单ID范围
public class OrderShardingStrategy {
public ShardingTarget route(Long userId, Long orderId) {
// 按用户ID哈希分库(保证同一用户的订单在同一个库)
int dbIndex = Math.abs(userId.hashCode() % 4);
// 按订单ID取模分表
int tableIndex = (int) (orderId % 16);
return new ShardingTarget("db_" + dbIndex, "order_" + tableIndex);
}
}
5.2 用户表分片策略
// 推荐:用户ID哈希取模
public class UserShardingStrategy {
public ShardingTarget route(Long userId) {
int dbIndex = Math.abs(userId.hashCode() % 4);
int tableIndex = (int) (userId % 8);
return new ShardingTarget("db_" + dbIndex, "user_" + tableIndex);
}
}
6. 核心要点总结
- 哈希取模是最常用的策略,数据分布均匀,但扩容困难
- 范围分片适合时间序列数据,如日志、订单归档
- 一致性哈希适合动态扩容场景,如缓存系统
- 分片键的选择至关重要:高基数、查询常用、不可变
- 实际应用中常采用复合策略:分库和分表使用不同的策略
- 预留扩展空间:初期可以分多一些表(如256张),按需启用