1. 分库分表技术背景与核心挑战
在数据量爆炸式增长的今天,单机数据库的存储和性能瓶颈日益凸显。我经历过一个电商项目,仅仅运行了半年,订单表就突破了2亿条记录,查询响应时间从最初的200ms飙升到5秒以上。这时候,分库分表就成了必选项而非可选项。
ShardingSphere作为Apache顶级开源项目,提供了完整的分库分表解决方案。但真正用好它,必须深入理解其两大核心机制:分片策略(Sharding Strategy)和分片算法(Sharding Algorithm)。这两个概念经常被混淆,实际上它们各司其职:
- 分片策略是"指挥官",决定数据应该按照什么规则分布(如按用户ID范围、按时间月份)
- 分片算法是"执行者",具体计算某条数据应该落到哪个分片(如取模运算、哈希映射)
2. 分片策略的战术选择
2.1 标准分片策略:精确路由的利器
标准分片策略(StandardShardingStrategy)是最常用的战术选择,适合明确知道分片键(sharding key)的场景。比如我们电商系统的订单表,选择user_id作为分片键,可以确保同一用户的订单总在同一个分片上。
配置示例:
yaml复制spring:
shardingsphere:
sharding:
tables:
t_order:
actual-data-nodes: ds_${0..1}.t_order_${0..15}
table-strategy:
standard:
sharding-column: user_id
precise-algorithm-class-name: com.example.algorithm.UserIdPreciseShardingAlgorithm
这里有个实战经验:分片键的选择需要同时考虑业务查询模式和数据分布均匀性。曾经有个项目用订单创建时间作为分片键,结果发现双11当天的流量全部集中在一个分片,导致热点问题。后来改为"用户ID后两位+月份"的复合分片策略才解决。
2.2 复合分片策略:多维路由方案
当单个分片键无法满足需求时,复合分片策略(ComplexShardingStrategy)就派上用场了。比如物流系统需要同时按发货地和收货地查询:
java复制public class LocationShardingAlgorithm implements ComplexKeysShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
ComplexKeysShardingValue<Long> shardingValue) {
// 获取发货地ID和收货地ID
Long fromLocation = shardingValue.getColumnNameAndShardingValuesMap().get("from_location_id").iterator().next();
Long toLocation = shardingValue.getColumnNameAndShardingValuesMap().get("to_location_id").iterator().next();
// 自定义路由逻辑
String suffix = (fromLocation % 10) + "_" + (toLocation % 10);
return availableTargetNames.stream()
.filter(each -> each.endsWith(suffix))
.collect(Collectors.toList());
}
}
注意:复合分片会增加SQL解析复杂度,建议在真正需要多维度查询时才使用。我曾见过一个系统把6个字段设为分片键,结果性能反而比单机还差。
2.3 行表达式分片策略:轻量级配置方案
对于简单的分片规则,可以使用行表达式策略(InlineShardingStrategy)避免编写Java代码。比如按用户ID的尾号分4个库:
yaml复制spring:
shardingsphere:
sharding:
tables:
t_user:
actual-data-nodes: ds_${0..3}.t_user
database-strategy:
inline:
sharding-column: user_id
algorithm-expression: ds_${user_id % 4}
这种方式的局限是只能实现简单的取模或范围分片。有个常见的坑点是:当需要扩容时,修改algorithm-expression会导致数据重分布。建议初期可以用行表达式快速验证,后期再切换为标准策略。
3. 分片算法的实现艺术
3.1 精确分片算法:点查询的基石
精确分片算法(PreciseShardingAlgorithm)处理=和IN操作,是实现高效单条查询的关键。以下是按时间范围分片的典型实现:
java复制public class DatePreciseShardingAlgorithm implements PreciseShardingAlgorithm<Date> {
private static final SimpleDateFormat YEAR_MONTH_FORMAT = new SimpleDateFormat("yyyyMM");
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<Date> shardingValue) {
// 按月分表,表后缀格式如 _202301
String suffix = YEAR_MONTH_FORMAT.format(shardingValue.getValue());
return availableTargetNames.stream()
.filter(each -> each.endsWith(suffix))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("未找到匹配分表"));
}
}
实际项目中,我们遇到过时区问题。服务器在UTC时区而业务用北京时间,导致每天有1小时的数据路由错误。解决方案是在算法中加入时区转换:
java复制TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
3.2 范围分片算法:区间查询的优化
范围分片算法(RangeShardingAlgorithm)处理BETWEEN、>、<等操作。以下是按数值范围分库的案例:
java复制public class AgeRangeShardingAlgorithm implements RangeShardingAlgorithm<Integer> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
RangeShardingValue<Integer> shardingValue) {
Range<Integer> range = shardingValue.getValueRange();
return availableTargetNames.stream()
.filter(each -> {
int dbSuffix = Integer.parseInt(each.substring(each.lastIndexOf("_") + 1));
// 分库规则:0-20岁在ds_0,21-40在ds_1,以此类推
return (dbSuffix * 20) <= range.upperEndpoint()
&& ((dbSuffix + 1) * 20) > range.lowerEndpoint();
})
.collect(Collectors.toList());
}
}
这里有个性能优化点:当范围跨度过大时,可能返回所有分片导致全库扫描。我们通过组合使用分片键(如age+region)来缩小范围。
3.3 自定义哈希算法:解决热点问题
标准的CRC32或MD5哈希可能导致数据倾斜。我们在社交系统中设计了一套加权哈希算法:
java复制public class WeightedHashShardingAlgorithm implements PreciseShardingAlgorithm<String> {
private final int[] weights = {3, 2, 2, 1}; // 四个分片的权重比
@Override
public String doSharding(Collection<String> availableTargetNames,
PreciseShardingValue<String> shardingValue) {
int hash = Math.abs(shardingValue.getValue().hashCode());
int sum = Arrays.stream(weights).sum();
int slot = hash % sum;
int accumulator = 0;
for (int i = 0; i < weights.length; i++) {
accumulator += weights[i];
if (slot < accumulator) {
return "ds_" + i;
}
}
throw new IllegalStateException("路由计算异常");
}
}
这种算法配合监控系统,可以动态调整权重。比如我们发现ds_0负载过高时,把它的权重从3降到2,新数据就会自动向其他分片倾斜。
4. 高级场景与性能优化
4.1 绑定表与广播表策略
在多表关联查询时,绑定表(Binding Table)能避免笛卡尔积查询。比如订单表和订单明细表都按order_id分片:
yaml复制spring:
shardingsphere:
sharding:
binding-tables:
- t_order, t_order_item
广播表(Broadcast Table)如地区编码表,会在所有库中保持同步:
yaml复制spring:
shardingsphere:
sharding:
broadcast-tables:
- t_region
踩坑记录:曾经有个项目把频繁更新的用户余额表设为广播表,结果每次更新都触发全库同步,性能直接崩盘。广播表只适合极少变更的字典表。
4.2 分片策略与索引的配合
分片键的选择需要与索引设计联动。最佳实践是:
- 分片键必须有索引
- 复合分片键的顺序要与联合索引顺序一致
- 避免在分片键上使用函数,会导致索引失效
我们通过EXPLAIN分析发现,以下配置会导致全表扫描:
sql复制-- 分片算法:order_id % 8
SELECT * FROM t_order WHERE SUBSTRING(order_id, 5, 1) = 'A';
解决方案是增加冗余列并建立索引:
sql复制ALTER TABLE t_order ADD COLUMN order_id_suffix CHAR(1) GENERATED ALWAYS AS (SUBSTRING(order_id, 5, 1));
CREATE INDEX idx_order_id_suffix ON t_order(order_id_suffix);
4.3 弹性伸缩与数据迁移
当现有分片不够用时,需要动态扩容。我们总结的平滑迁移方案:
- 双写模式:新老分片同时写入
- 数据校验:使用checksum比对数据一致性
- 灰度切流:逐步将查询切到新分片
ShardingSphere 5.x提供的Scaling功能可以自动化这个过程:
bash复制curl -X POST http://localhost:8888/shardingscaling/job/start \
-H "Content-Type: application/json" \
-d '{
"ruleConfiguration": {
"sourceDatasource": "ds_0",
"sourceTable": "t_order",
"targetDatasource": "ds_4",
"targetTable": "t_order"
},
"jobConfiguration": {
"concurrency": 3
}
}'
5. 监控与问题排查
5.1 慢查询分析
通过开启SQL日志可以定位分片问题:
properties复制spring.shardingsphere.props.sql.show=true
典型问题包括:
- 全路由:没有使用分片键导致查询所有分片
- 笛卡尔积:关联表未正确配置绑定关系
- 内存溢出:大批量数据跨分片排序
我们开发了自定义监控模块,统计各分片的查询耗时,当发现某个分片响应时间突增时自动告警。
5.2 分布式事务一致性
在跨分片更新时,需要考虑事务一致性。ShardingSphere支持多种方案:
- XA强一致(性能较差)
- Seata AT模式(推荐)
- 最大努力送达(最终一致)
配置示例:
yaml复制spring:
shardingsphere:
props:
xa-transaction-manager-type: Atomikos
实际项目中,我们采用"本地事务+异步补偿"的混合方案。比如支付成功后,先扣减本地分片库存,然后通过消息队列异步同步其他分片。
5.3 影子库压测方案
上线前必须用影子库(Shadow DB)进行压力测试。配置方式:
yaml复制spring:
shardingsphere:
shadow:
enable: true
data-sources:
production-ds:
source-data-source-name: ds-real
shadow-data-source-name: ds-shadow
tables:
t_order:
shadow-algorithm-names: [simple-hint-algorithm]
压测时在SQL中添加hint注释即可路由到影子库:
sql复制/* SHARDINGSPHERE_HINT: SHADOW=true */
SELECT COUNT(*) FROM t_order;