分片算法与ShardingJDBC路由策略
分表算法核心原则
分表算法的本质是将数据路由到具体的物理表。无论选择何种算法,都必须保证:同一分表字段值经过算法处理后,结果必须唯一且不可变。
假设对product表分成128张表,分表结果应为:product_0000、product_0001、product_0002...product_0126、product_0127
常见分表算法详解
直接取模算法
最简单直接的分表方式,适用于分表字段为整数类型的场景。
算法原理:
// 示例:库存记录表分128张表
public String getTableName(Long warehouseId) {
int tableIndex = (int)(warehouseId % 128);
return "t_inventory_" + String.format("%04d", tableIndex);
}
适用场景:
- 分表字段为数值型ID
- 数据分布相对均匀
- 简单高效,计算开销小
哈希取模算法
当分表字段为字符串类型时,先计算哈希值,再对分表数取模。
算法实现:
// 示例:设备记录表按设备编号分表
public String getTableName(String deviceCode) {
// 注意:Java hashCode可能返回负数
int hash = deviceCode.hashCode();
int tableIndex = Math.abs(hash) % 256;
return "t_device_log_" + String.format("%04d", tableIndex);
}
关键注意点:
Java的hashCode()方法可能返回负数,需要取绝对值:
// 错误示例
int tableIndex = deviceCode.hashCode() % 256; // 可能得到负数
// 正确示例
int tableIndex = Math.abs(deviceCode.hashCode()) % 256;
按关键字分表
根据业务特征字段直接分表,常见于时间维度或地域维度。
时间维度分表:
// 示例:日志表按月份分表
public String getTableName(Date logTime) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM");
String monthStr = sdf.format(logTime);
return "t_access_log_" + monthStr;
}
生成的表名如:t_access_log_202401、t_access_log_202402
地域维度分表:
// 示例:门店数据按大区分表
public String getTableName(String regionCode) {
return "t_store_" + regionCode.toLowerCase();
}
生成的表名如:t_store_east、t_store_west、t_store_south
适用场景:
- 数据归档需求明确(如按月归档)
- 查询通常带时间范围或地域条件
- 便于数据生命周期管理
一致性哈希算法
专门为解决扩容问题设计的算法,能在节点变化时最小化数据迁移量。
核心思想:
将哈希值空间组织成一个首尾相接的环(0 ~ 2^32-1),数据和节点都映射到环上,数据顺时针寻找最近节点存储。
扩容优势:
假设原有4个节点,扩容到8个节点时:
- 传统取模:几乎所有数据需要重新路由迁移
- 一致性哈希:平均只需迁移50%数据
代码示例:
// 简化的一致性哈希实现
public class ConsistentHash {
private TreeMap<Long, String> ring = new TreeMap<>();
private int virtualNodes = 150; // 虚拟节点数
// 添加物理节点
public void addNode(String nodeName) {
for (int i = 0; i < virtualNodes; i++) {
String virtualNodeName = nodeName + "#" + i;
long hash = hash(virtualNodeName);
ring.put(hash, nodeName);
}
}
// 获取数据应该路由的节点
public String getNode(String key) {
long hash = hash(key);
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);
}
}
ShardingJDBC分片策略
ShardingJDBC将分片策略抽象为"分片算法 + 分片键"的组合,提供了丰富的内置支持。
四种分片算法
精确分片算法
处理SQL中的等值查询(=)和IN查询。
// 实现精确分片算法
public class ProductPreciseShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Long> shardingValue) {
Long productId = shardingValue.getValue();
int tableIndex = (int)(productId % 128);
String targetTable = "t_product_" + String.format("%04d", tableIndex);
if (availableTargetNames.contains(targetTable)) {
return targetTable;
}
throw new IllegalArgumentException("Table not found: " + targetTable);
}
}
范围分片算法
处理SQL中的范围查询(BETWEEN AND、>、<等)。
// 实现范围分片算法
public class ProductRangeShardingAlgorithm implements RangeShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
RangeShardingValue<Long> shardingValue) {
Range<Long> range = shardingValue.getValueRange();
Set<String> result = new HashSet<>();
// 根据范围计算涉及的表
Long lowerBound = range.lowerEndpoint();
Long upperBound = range.upperEndpoint();
for (long id = lowerBound; id <= upperBound; id++) {
int tableIndex = (int)(id % 128);
String tableName = "t_product_" + String.format("%04d", tableIndex);
result.add(tableName);
}
return result;
}
}
复合分片算法
支持多个分片键的复杂场景,应用开发者需自行处理多键之间的逻辑关系。
// 复合分片:同时基于产品ID和仓库ID
public class ProductWarehouseComplexShardingAlgorithm
implements ComplexKeysShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
ComplexKeysShardingValue<Long> shardingValue) {
Collection<Long> productIds = shardingValue.getColumnNameAndShardingValuesMap()
.get("product_id");
Collection<Long> warehouseIds = shardingValue.getColumnNameAndShardingValuesMap()
.get("warehouse_id");
Set<String> result = new HashSet<>();
// 自定义复合逻辑
for (Long productId : productIds) {
for (Long warehouseId : warehouseIds) {
long combinedKey = productId ^ warehouseId; // 异或组合
int tableIndex = (int)(combinedKey % 256);
result.add("t_inventory_" + String.format("%04d", tableIndex));
}
}
return result;
}
}
Hint分片算法
通过编程方式强制指定路由目标,绕过SQL解析。
// Hint分片使用示例
HintManager hintManager = HintManager.getInstance();
// 强制路由到指定表
hintManager.addTableShardingValue("t_product", 15);
// 执行查询,将路由到 t_product_0015
List<Product> products = productMapper.queryByCondition(condition);
hintManager.close();
五种分片策略
标准分片策略
单分片键,支持=、IN、BETWEEN AND操作。
配置示例:
spring:
shardingsphere:
rules:
sharding:
tables:
t_product:
actual-data-nodes: db0.t_product_$->{0..127}
table-strategy:
standard:
sharding-column: product_id
sharding-algorithm-name: product-inline
sharding-algorithms:
product-inline:
type: INLINE
props:
algorithm-expression: t_product_$->{product_id % 128}
行表达式分片策略
最常用的策略,通过Groovy表达式配置,无需编写Java代码。
配置示例:
# 库存表按仓库ID模8分表
sharding-algorithms:
inventory-inline:
type: INLINE
props:
algorithm-expression: t_inventory_$->{warehouse_id % 8}
表达式t_inventory_$->{warehouse_id % 8}会生成:t_inventory_0 到 t_inventory_7
适用场景:
- 简单的取模分表
- 快速配置,维护成本低
复合分片策略
项目中使用频率高,支持多分片键场景。
**应用场景:**基因法中同时使用业务ID和订单号分片
// 配置复合分片
@Bean
public ShardingRuleConfiguration shardingRuleConfig() {
ShardingRuleConfiguration config = new ShardingRuleConfiguration();
ShardingTableRuleConfiguration tableConfig =
new ShardingTableRuleConfiguration("t_order", "db0.t_order_$->{0..255}");
// 复合分片策略
ComplexShardingStrategyConfiguration strategyConfig =
new ComplexShardingStrategyConfiguration("order_id,customer_id", "orderComplexAlgorithm");
tableConfig.setTableShardingStrategy(strategyConfig);
config.getTableRuleConfigs().add(tableConfig);
return config;
}
Hint分片策略
需要明确指定物理表时使用,场景包括:
- 数据修复,直接操作某个物理表
- 性能优化,已知数据位置时跳过路由计算
- 灰度验证,指定部分表执行新逻辑
避免数据倾斜的分片设计
选择高散列性分片键
前文已详细讨论,核心是选择数据分布均匀的字段。
复合分片
对可能产生热点的字段,叠加时间或其他维度二次分散:
// 针对热点客户的复合分片
public int calculateShard(Long customerId, CustomerType type, Date createTime) {
if (type == CustomerType.VIP && isHotCustomer(customerId)) {
// VIP大客户:叠加日期分片
long dateFactor = DateUtils.truncate(createTime, Calendar.DAY_OF_MONTH).getTime();
return (int)((customerId + dateFactor) % 512);
} else {
// 普通客户:仅基于ID
return (int)(customerId % 512);
}
}
虚拟分片
在物理分片基础上增加虚拟层,平滑数据分布:
// 虚拟分片配置
private static final int VIRTUAL_SHARDS = 1000;
private static final int PHYSICAL_SHARDS = 128;
public String getPhysicalTable(Long recordId) {
// 虚拟分片
int virtualShard = (int)(recordId % VIRTUAL_SHARDS);
// 映射到物理分片
int physicalShard = virtualShard % PHYSICAL_SHARDS;
return "t_record_" + String.format("%04d", physicalShard);
}
即使某些ID段密集,经过虚拟分片也能较均匀分布。
ShardingJDBC无分片键查询
当SQL不包含分片键时,ShardingJDBC如何处理?
广播路由机制
ShardingJDBC的路由引擎会根据SQL类型采用不同广播策略:
DML全库表路由示例
逻辑表t_order对应物理表:t_order_00, t_order_01, t_order_02, t_order_03
执行SQL:
SELECT * FROM t_order WHERE customer_name = 'Alice';
实际执行:
SELECT * FROM t_order_00 WHERE customer_name = 'Alice'
UNION ALL
SELECT * FROM t_order_01 WHERE customer_name = 'Alice'
UNION ALL
SELECT * FROM t_order_02 WHERE customer_name = 'Alice'
UNION ALL
SELECT * FROM t_order_03 WHERE customer_name = 'Alice'
性能影响:
- 需要扫描所有物理表
- 网络开销和计算资源消耗大
- 应尽量避免生产环境使用
解决方案:
- 查询条件必须包含分片键
- 非分片键查询转移到ES等搜索引擎
- 建立冗余的多维度分表