跳转至

分库分表自定义策略

首先捋一下整体的分库分表的思路:

针对link_group分组的数据,根据用户的account_no字段进行分库,前面有提到过用户的account_no字段是一串数字,所以根据其对2取余得到的结果进行分库

针对short_link短链的数据,根据短链码code进行分库分表,在进行短链码生成的时候可以在其前后分别添加一位,前面一位用于标识库位,后面一位用于标识表位。

至于分库分表的逻辑则是在DataSourceConfig数据源配置中进行配置

首先创建两个数据源配置

private DataSource createDataSource0() {
        com.zaxxer.hikari.HikariDataSource ds = new com.zaxxer.hikari.HikariDataSource();
        ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
        ds.setJdbcUrl(dbUrl1);
        ds.setUsername(dbUser);
        ds.setPassword(dbPassword);

        // 添加连接池优化配置 [8](@ref)
        ds.setMinimumIdle(5);                // 最小空闲连接数
        ds.setMaximumPoolSize(20);           // 最大连接数
        ds.setIdleTimeout(30000);            // 空闲连接超时时间(毫秒)
        ds.setMaxLifetime(1800000);          // 连接最大生命周期(毫秒)
        ds.setConnectionTimeout(30000);      // 连接获取超时时间(毫秒)
        ds.setLeakDetectionThreshold(60000); // 连接泄漏检测阈值(毫秒)
        ds.setConnectionTestQuery("SELECT 1"); // 连接验证查询
        return ds;
    }
    private DataSource createDataSource1() {
        com.zaxxer.hikari.HikariDataSource ds = new com.zaxxer.hikari.HikariDataSource();
        ds.setDriverClassName("com.mysql.cj.jdbc.Driver");
        ds.setJdbcUrl(dbUrl2);
        ds.setUsername(dbUser);
        ds.setPassword(dbPassword);

        // 添加连接池优化配置 [8](@ref)
        ds.setMinimumIdle(5);                // 最小空闲连接数
        ds.setMaximumPoolSize(20);           // 最大连接数
        ds.setIdleTimeout(30000);            // 空闲连接超时时间(毫秒)
        ds.setMaxLifetime(1800000);          // 连接最大生命周期(毫秒)
        ds.setConnectionTimeout(30000);      // 连接获取超时时间(毫秒)
        ds.setLeakDetectionThreshold(60000); // 连接泄漏检测阈值(毫秒)
        ds.setConnectionTestQuery("SELECT 1"); // 连接验证查询
        return ds;
    }

整体的分库分表逻辑配置

        // 1. 配置数据源
        Map<String, DataSource> dataSourceMap = new HashMap<>();
        // 通过put,可以配置多个数据源
        dataSourceMap.put("ds0", createDataSource0());
        dataSourceMap.put("ds1", createDataSource1());

        // 2. 配置分片规则
        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
        shardingRuleConfig.getTableRuleConfigs().add(getLinkGroupRuleConfiguration());
        shardingRuleConfig.getTableRuleConfigs().add(getShortLinkRuleConfiguration());
        // 添加多个表的分片规则,可以配置多个方法
        // shardingRuleConfig.getTableRuleConfigs().add(getTableRuleConfiguration2());

        Properties properties = new Properties();
        // 自定义方法生成workId
        properties.setProperty("worker.id", snowFlakeWordIdConfig.getWorkerId()); // 设置工作节点ID

        // 配置主键生成规则
        KeyGeneratorConfiguration keyGeneratorConfiguration = new KeyGeneratorConfiguration(
                "SNOWFLAKE",  // 内置雪花算法类型生成id
                "id",         // 主键字段名
                properties    // 配置属性
        );

       // 直接设置配置对象,而非生成器实例
        shardingRuleConfig.setDefaultKeyGeneratorConfig(keyGeneratorConfiguration);

        // 3. 创建 ShardingSphere 数据源
        return ShardingDataSourceFactory.createDataSource(
                dataSourceMap,
                shardingRuleConfig,
                new Properties()
        );

针对LinkGroup和ShortLink的分库分表规则

private TableRuleConfiguration getLinkGroupRuleConfiguration() {
        // 使用正确的分表表达式
        TableRuleConfiguration result = new TableRuleConfiguration(
                "link_group",
                "ds$->{0..1}.link_group"
        );
        // 设置分库策略,根据account_no字段进行分库
        result.setDatabaseShardingStrategyConfig(
                new InlineShardingStrategyConfiguration("account_no", "ds$->{account_no % 2}")
        );
        return result;
    }

private TableRuleConfiguration getShortLinkRuleConfiguration() {
    StandardShardingStrategyConfiguration databaseShardingStrategy =
        new StandardShardingStrategyConfiguration(
        "code",
        new CustomDBPreciseShardingAlgorithm()  // 实例化您的自定义分库算法
    );
    StandardShardingStrategyConfiguration tableShardingStrategy =
        new StandardShardingStrategyConfiguration(
        "code",
        new CustomTablePreciseShardingAlgorithm()  // 实例化您的自定义分库算法
    );
    // 关键修复:使用正确的分表表达式
    TableRuleConfiguration result = new TableRuleConfiguration(
        "short_link",
        "ds$->{0..1}.short_link_$->{0..1}"
    );
    // 设置分库策略,根据短链码code字段进行分库
    result.setDatabaseShardingStrategyConfig(
        databaseShardingStrategy
    );
    // 设置分表策略,根据短链码code字段进行分表
    result.setTableShardingStrategyConfig(
        tableShardingStrategy
    );
    return result;
}
// ShortLink的自定义分片规则
// 分库规则
public class CustomDBPreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {
    /**
     *
     * @param collection 数据源集合
     *                   在分库时值为所有分片库的集合 databaseNames
     *                   分表时为对应分库中所有分片表的集合 tablesNames
     *                   分库场景:值为所有分片库的名称集合(如 ["ds0", "ds1", "ds2"])。
     *                   分表场景:值为当前分片库中所有分片表的名称集合(如 ["t_order_0", "t_order_1"])。
     * @param preciseShardingValue 分片的属性
     *                             logicTableName(逻辑表名,如 t_order)
     *                             columnName(分片键字段名,如 short_code)
     *                             value(分片键的具体值,如 "A123456")
     * @return
     */
    @Override
    public String doSharding(Collection<String> collection, PreciseShardingValue<String> preciseShardingValue) {
        // 获取短链码第一位,即库位
        String codePrefix = preciseShardingValue.getValue().substring(0,1);
        for (String targetName:collection) {
            //获取库名的最后一位
            String targetNameSuffix = targetName.substring(targetName.length()-1);
            if (codePrefix.equals(targetNameSuffix)){
                return  targetName;
            }
        }
        return null;
    }
}
// 分表规则
public class CustomTablePreciseShardingAlgorithm implements PreciseShardingAlgorithm<String> {
    /**
     *
     * @param availableTargetNames 数据源集合
     *                   在分库时值为所有分片库的集合 databaseNames
     *                   分表时为对应分库中所有分片表的集合 tablesNames
     *                   分库场景:值为所有分片库的名称集合(如 ["ds0", "ds1", "ds2"])。
     *                   分表场景:值为当前分片库中所有分片表的名称集合(如 ["t_order_0", "t_order_1"])。
     * @param shardingValue 分片的属性
     *                             logicTableName(逻辑表名,如 t_order)
     *                             columnName(分片键字段名,如 short_code)
     *                             value(分片键的具体值,如 "A123456")
     * @return
     */
    @Override
    public String doSharding(Collection<String> availableTargetNames,
                             PreciseShardingValue<String> shardingValue) {
        // 1. 获取分片键值(短链码)
        String code = shardingValue.getValue();

        // 2. 提取表后缀(如取最后一位)
        String tableSuffix = code.substring(code.length() - 1); // 示例:code="ABC123" → "3"

        // 3. 构建目标表名基础(逻辑表名_后缀)
        String targetBaseName = shardingValue.getLogicTableName() + "_" + tableSuffix;

        // 4. 遍历所有可用物理表名,匹配后缀
        for (String physicalTable : availableTargetNames) {
            // physicalTable 格式:ds0.short_link_1
            if (physicalTable.endsWith(targetBaseName)) { // 后缀匹配
                return physicalTable; // 返回完整物理表名
            }
        }

        throw new IllegalStateException("无法路由到分片表:" + code);
    }
}

在进行分库分表后,会有几个问题;一是库表扩容后,数据分布不均匀的问题、二是分库分表后进行数据查询时如果进行全库表查询的话,会很耗费性能

针对问题一:最直接的方法是增加新建库表的权重比,可以通过负载均衡端实现,也可自定义算法实现。我这边是使用的第二个方法

在进行库表名拼接的时候是随机返回标识,0/1,根据返回的标识进行库表名的拼接。增加权重比的话,直接在list中新增对应的0/1,增加对应表的权重比

    private static  final List<String> DBPrefixList = new ArrayList<>();

    static {
        DBPrefixList.add("0");
        DBPrefixList.add("1");
    }

    private static Random random = new Random();

    public static String getRandomPrefix(){
        int index = random.nextInt(DBPrefixList.size());
        return DBPrefixList.get(index);
    }
    private static  final List<String> TablePrefixList = new ArrayList<>();

    static {
        TablePrefixList.add("0");
        TablePrefixList.add("1");
    }

    private static Random random = new Random();

    public static String getRandomPrefix(){
        int index = random.nextInt(TablePrefixList.size());
        return TablePrefixList.get(index);
    }

针对问题二:可以引入nosql,比如es,对数据全量冗余存储。在进行查询时,不通过数据库,直接在es中进行查询。也可进行双向冗余,比如订单系统,针对商家和客户两个维度将订单在两个地方都存储一份。在查询时,既可根据客户主键查询订单,也可通过商户逐渐查询订单,通过空间换取查询时的高效性能。