wb.ouyang

毕竟几人真得鹿,不知终日梦为鱼

导航

案例: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位表索引,然后匹配对应的表。

image

 

image

 分库算法 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) {
    }
}
View Code

分表算法 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) {
    }
}
View Code

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

image

 

参考:

1)分库分表下,扩容数据免迁移方案-腾讯云开发者社区-腾讯云

posted on 2025-12-25 23:28  wenbin_ouyang  阅读(2)  评论(0)    收藏  举报