案例:shardingSphere扩容数据免迁移,无数据冷热不均
1、ShardingSphere 分库分表扩容问题分析
1.1、哈希取模分片(Hash Mod Sharding)扩容问题
- 数据重哈希迁移:
当分片数量(N)增加时(如从4增加到5),取模基数变化(%4 → %5),导致几乎所有历史数据需重新计算哈希值并迁移至新分片,成本极高。
- 示例:
// 原分片规则:user_id % 4 // 新分片规则:user_id % 5 // 旧数据需重新计算哈希值,迁移至新分片
- 性能开销:
扩容期间需停机或双写模式(新旧分片同时写入),验证数据一致性,影响业务稳定性。
- 路由规则更新:
需动态更新路由配置,通过配置中心(如Zookeeper)通知所有节点。
1.2、范围分片(Range Sharding)
扩容问题- 数据热点:
按时间分片时,所有新写入请求集中于最新分片,造成写入压力。
- 示例:
-- 按时间范围分片 SELECT * FROM orders WHERE create_time BETWEEN '2024-01-01' AND '2024-03-31' -- 新订单集中写入orders_2024_q1
- 扩容复杂性:
需新增分片范围(如2024年Q4分片),但历史数据无需迁移,扩容相对简单。
- 示例:
-- 新增分片规则:2024年Q4 → orders_2024_q4 -- 无需迁移历史数据
总结
- 哈希分片扩容:
数据重哈希迁移成本高,需停机或双写模式,路由规则动态更新。
- 范围分片扩容:
写入热点问题,但新增分片无需数据迁移,扩容相对简单。
2、shardingSphere扩容数据免迁移方案
2.1、具体方案设计
分片键为user_id,生成策略为:8位随机字符串 + 2位库索引 + 2位表索引(具体如何生成看后面的UserHepler类)。
自定义分库算法,拿到userId后,从中取出2位库索引,然后匹配对应的库。
自定义分表算法,拿到userId后,从中取出2位表索引,然后匹配对应的表。


分库算法 CustomDBShardingAlgorithm
package com.starter.shardingdemo.algo; import lombok.extern.slf4j.Slf4j; import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue; import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue; import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm; import java.util.Collection; import java.util.List; import java.util.Properties; @Slf4j public class CustomDBShardingAlgorithm implements StandardShardingAlgorithm<String> { @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) { //目标名称:db or table log.info(">>>>>> available_target_names= [{}]", String.join(",", availableTargetNames)); //分片信息:分片表、分片列、分片值 String logicTableName = shardingValue.getLogicTableName(); String columnName = shardingValue.getColumnName(); String value = shardingValue.getValue(); log.info(">>>>>> logic_table= [{}], column_name= [{}], value= [{}]", logicTableName, columnName, value); for (String targetName : availableTargetNames) { // Step-1: 取出UserId中的库索引,比如 1,2,11,12 等 String prefix; if (value.charAt(8) == '0') { prefix = value.substring(9, 10); // 去除前面的0 } else { prefix = value.substring(8, 10); } // Step-2: 匹配目标并返回 String targetIndex = targetName.substring(2); // 去除"ds" if (targetIndex.equals(prefix)) { log.info(">>>>>> [{}] Use Target [{}]", value, targetName); return targetName; } } throw new UnsupportedOperationException("不支持的分片[" + value + "]"); } @Override public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<String> rangeShardingValue) { return List.of(); } @Override public Properties getProps() { return null; } @Override public void init(Properties properties) { } }
分表算法 CustomTBShardingAlgorithm
package com.starter.shardingdemo.algo; import lombok.extern.slf4j.Slf4j; import org.apache.shardingsphere.sharding.api.sharding.standard.PreciseShardingValue; import org.apache.shardingsphere.sharding.api.sharding.standard.RangeShardingValue; import org.apache.shardingsphere.sharding.api.sharding.standard.StandardShardingAlgorithm; import java.util.Collection; import java.util.List; import java.util.Properties; @Slf4j public class CustomTBShardingAlgorithm implements StandardShardingAlgorithm<String> { @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<String> shardingValue) { //目标名称:db or table log.info(">>>>>> available_target_names= [{}]", String.join(",", availableTargetNames)); //分片信息:分片表、分片列、分片值 String logicTableName = shardingValue.getLogicTableName(); String columnName = shardingValue.getColumnName(); String value = shardingValue.getValue(); log.info(">>>>>> logic_table= [{}], column_name= [{}], value= [{}]", logicTableName, columnName, value); for (String targetName : availableTargetNames) { // Step-1: 取出UserId中的表索引,比如 1,2,11,12 等 String prefix; if (value.charAt(10) == '0') { prefix = value.substring(11, 12); } else { prefix = value.substring(10, 12); } // Step-2: 匹配目标并返回 String targetIndex = targetName.substring(targetName.indexOf("_") + 1); if (targetIndex.equals(prefix)) { log.info(">>>>>> [{}] Use Target [{}]", value, targetName); return targetName; } } throw new UnsupportedOperationException("不支持的分片[" + value + "]"); } @Override public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<String> rangeShardingValue) { return List.of(); } @Override public Properties getProps() { return null; } @Override public void init(Properties properties) { } }
UserHelper类:提供生成UserId的方法,实际生产中生成8位随机字符串不能重复需要另外实现,此处只为了简单演示
package com.starter.shardingdemo.utils; import java.util.Random; public class UserHelper { private static final String CHARACTERS = "0123456789abcdefghijklmnopqrstuvwxyz"; private static final int LENGTH = 8; private static final Random random = new Random(); /** * 生成12位UserId:8位随机字符串 + 2位库索引 + 2位表索引 */ public static String generateUserId() { return generateRandomString() + generateUserIdShardingCode(); } /** * 生成一个8位的随机字符串,每一个字符只可以使用0-9,a-z范围的字符 */ public static String generateRandomString() { StringBuilder sb = new StringBuilder(LENGTH); for (int i = 0; i < LENGTH; i++) { int index = random.nextInt(CHARACTERS.length()); sb.append(CHARACTERS.charAt(index)); } return sb.toString(); } private static String generateUserIdShardingCode() { return ShardingDBConfig.getRandomDBSuffix() + ShardingTableConfig.getRandomTableSuffix(); } }
ShardingDBConfig:获取2位库索引
package com.starter.shardingdemo.utils; import java.util.ArrayList; import java.util.List; import java.util.Random; public class ShardingDBConfig { /** * 存储数据库位置编号 */ private static final List<String> dbSuffixList = new ArrayList<>(); private static final Random random = new Random(); //启用哪些库的后缀 static { dbSuffixList.add("0"); dbSuffixList.add("1"); dbSuffixList.add("2"); dbSuffixList.add("3"); // dbSuffixList.add("4"); // dbSuffixList.add("5"); // dbSuffixList.add("6"); // dbSuffixList.add("7"); // dbSuffixList.add("8"); // dbSuffixList.add("9"); // dbSuffixList.add("10"); dbSuffixList.add("11"); } /** * 获取随机的前缀,两位,不足两位的前面补0 */ public static String getRandomDBSuffix() { int index = random.nextInt(dbSuffixList.size()); String s = dbSuffixList.get(index); if (s.length() == 1) { return "0" + s; } return s; } }
ShardingTableConfig:获取2位表索引
package com.starter.shardingdemo.utils; import java.util.ArrayList; import java.util.List; import java.util.Random; public class ShardingTableConfig { /** * 存储数据库位置编号 */ private static final List<String> tableSuffixList = new ArrayList<>(); private static final Random random = new Random(); //启用哪些表的后缀 static { tableSuffixList.add("0"); tableSuffixList.add("1"); tableSuffixList.add("2"); tableSuffixList.add("3"); // tableSuffixList.add("4"); // tableSuffixList.add("5"); // tableSuffixList.add("6"); // tableSuffixList.add("7"); // tableSuffixList.add("8"); // tableSuffixList.add("9"); // tableSuffixList.add("10"); tableSuffixList.add("11"); } /** * 获取随机的后缀,两位,不足两位的前面补0 */ public static String getRandomTableSuffix() { int index = random.nextInt(tableSuffixList.size()); String s = tableSuffixList.get(index); if (s.length() == 1) { return "0" + s; } return s; } }
2.2、测试
/** * 测试不同分片键的数据分布 */ @GetMapping("/test-distribution") public String testDistribution() { // 测试不同user_id的数据分布 for (long i = 1; i <= 200; i++) { User user = new User(); user.setUserId(UserHelper.generateUserId()); user.setUsername("test_user_" + i); user.setEmail("test" + i + "@example.com"); user.setPhone("138" + String.format("%08d", i)); userService.addUser(user); } return "测试数据生成完成,观察不同user_id的分布情况"; }
2.3、代码
https://gitee.com/sanlinspace/sharding-sphere_user.git

参考:
posted on 2025-12-25 23:28 wenbin_ouyang 阅读(2) 评论(0) 收藏 举报
浙公网安备 33010602011771号