Sharding-JDBC分库分表之SpringBoot分片策略

Sharding-JDBC系列

1、Sharding-JDBC分库分表的基本使用

2、Sharding-JDBC分库分表之SpringBoot分片策略

3、Sharding-JDBC分库分表之SpringBoot主从配置

前言

前一篇以一个示例分享了Sharding-JDBC的基本使用。在进行分库分表时,可以设置分库分表的分片策略,在示例中,使用的是最简单的inline分片策略。本篇详细的给大家分享一下Sharding-JDBC的分片策略。

核心概念

在开始讲解分片策略之前,先熟悉几个核心概念。

2.1 逻辑表

水平拆分表后,每个小的表的结构都是一样的,这些相同结构的小表可以使用一个逻辑表来表示,是SQL中表的逻辑标识。

如订单表通过主键按照一定规则(如模5)分为tb_order_1到tb_order_5,它们可以使用同一个逻辑表tb_order来标识。

2.2 真实表

在水平拆分的数据库中真实存在的物理表。

如上面的订单表示例,tb_order1到tb_order_5表为真实表。

2.3 分片键

用于将数据库(表)拆分(水平、垂直)的字段即为分片键。

如上面的订单表示例,订单主键为分片键。

如果没有分片键,所有的数据操作将执行全路由,性能较差。除了对单分片字段的支持,Apache ShardingSphere 也支持根据多个字段进行分片。

2.4 分片算法

用于将数据分片的算法,支持 =>=<=><BETWEENIN 进行分片。Sharding-JDBC不断完善实现自动分片算法,在最新的5.5.0中,提供了基于取模、基于哈希取模、基于分片边界、基于分片容量、基于可变时间的分片算法。开发者也可以通过实现基础的分片算法接口,自定义分片算法。

2.5 分片策略

分片策略由分片键和分片算法组成。分片键是从表格的字段中定义,分片算法是相对独立,可自定义的。不同的分片算法,对应了不同的分片策略。

在Sharding-JDBC中,分片策略分为四类,分别为行表达式分片策略、标准分片策略、复合分片策略、Hint分片策略。不同的分片算法构成了不同的分片策略,但所有的算法都归类到上面的四种类型中,只是具体的实现不同。

准备工作

以下以订单为例,分享一下四类分片策略的使用。先创建表tb_order_1、tb_order_2、tb_order_0_0、tb_order_0_1、tb_order_1_0、tb_order_1_1六张表。

行表达式分片策略

行表达式分片策略(InlineShardingStrategy),无需自定义分片算法,框架已默认实现,适用于做简单的分片算法,是四类分片策略中最简单的。

使用时,在配置中使用Groovy表达式,默认提供对SQL语句中分片键的=和IN的分片操作支持,仅支持单分片键。

可以通过设置allow.range.query.with.inline.sharding=true,设置支持 BETWEEN AND、>、<、>=、<= 的分片操作

4.1 Groovy表达式概述

行表达式的使用非常直观,只需要在配置中使用 ${ expression } 或 $->{ expression } 标识行表达式即可。目前支持数据节点和分片算法这两个部分的配置。行表达式的内容使用的是 Groovy 的语法,Groovy 能够支持的所有操作,行表达式均能够支持。

如:${begin..end} 表示范围区间、${[unit1, unit2, unit_x]} 表示枚举值。

${['tb_order', 'tb_order_goods']}_${1..2},最终解析为:tb_order_1、tb_order_2、tb_order_goods_1、tb_order_goods_2

行表达式中如果出现连续多个 ${ expression } 或 $->{ expression } 表达式,整个表达式最终的结果将会根据每个子表达式的结果进行笛卡尔组合。

4.2 示例

行表达式分片策略使用时非常简洁,分片配置如下:

spring:
  shardingsphere:
    datasource:
      names: order1,order2
    sharding:
      tables:
        tb_order: #逻辑表
          database-strategy: #分库策略
            inline:  # 行表达式分片策略
              sharding-column: member_id
              algorithm-expression: order$->{member_id % 2 + 1} #以member_id模2 + 1,如member_id为3,则存放在order2数据库中
          actual-data-nodes: order$->{1..2}.tb_order_$->{1..2}
          table-strategy: #分表策略
            inline:  # 行表达式分片策略
              sharding-column: order_id
              algorithm-expression: tb_order_$->{order_id % 2 + 1}

1)分库策略

spring.shardingsphere.sharding.tables.tb_order.database-strategy.inline

2)分表策略

spring.shardingsphere.sharding.tables.tb_order.table-strategy.inline

3)其他配置

sharding-column分片键,表中的字段

algorithm-expression:算法表达式,Groovy表达式。tb_order_$->{order_id % 2 + 1},表示以order_id模2后加1为分片算法。如order_id为2,则操作tb_order_1的表

详细的示例见:

Sharding-JDBC分库分表的基本使用-CSDN博客

注:行表达式分片策略仅支持SQL中分片键包含 =、IN 的分片处理,对于 > 、< 等的SQL语句,系统会报错。

标准分片策略

标准分片策略(StandardShardingStrategy),用于处理使用单一键作为分片键的 =、IN、BETWEEN AND、>、<、>=、<= 进行分片的场景。

标准分片策略提供了PreciseShardingAlgorithm(精准分片)和 RangeShardingAlgorithm(范围分片)两种分片算法接口。

使用时,精准分片算法是必现实现的算法,用于处理SQL中分片键含=、IN 的分片处理。范围分片算法用于处理SQL中分片键含 >、<、>=、<= 、BETWEEN AND的分片处理,是非必选的。

注:如果没有实现范围分片算,而SQL中使用了BETWEEN AND的分片处理,那么系统会报错。

说明:在最新的5.5.0的版本,标准分片策略整合了精准分片和范围分片,对应的算法接口为StandardShardingAlgorithm。

5.1 精准分片算法

通过实现PreciseShardingAlgorithm接口实现精准分片算法,该接口只有一个方法,源码如下:

public interface PreciseShardingAlgorithm<T extends Comparable<?>> extends ShardingAlgorithm {
    /**
     * @param availableTargetNames 可用的datasource或table的名称集合
     * @param shardingValue sql中,传入的分片键的值
     * @return
     */
    String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<T> shardingValue);
}

对于数据库分片,第一个参数为配置的可用的数据库的名称集合;对于表分片,第一个参数为配置的可用的表的名称集合。所以对于数据库分片和表分片,实现逻辑一样,此处以表分片为例。

5.1.1 自定义表分片算法

package com.jingai.sharing.jdbc.algorithm;

public class StandardShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
    @Override
    public String doSharding(Collection<String> tableNames, PreciseShardingValue<Long> preciseShardingValue) {
        for (String tableName : tableNames) {
            String mod = preciseShardingValue.getValue() % tableNames.size() + 1 + Strings.EMPTY;
            if(tableName.endsWith(mod)) {
                return tableName;
            }
        }
        throw new IllegalArgumentException("找不到匹配的表分片");
    }
}

以上通过分片键对真实表个数取模加1作为分片的算法。

如:有两个表tb_order_1和tb_order_2,传入的分片键的值为3,则返回tb_order_2的表名字符串

5.1.2 分片配置

spring:
  shardingsphere:
    sharding:
      tables:
        tb_order: #逻辑表
          table-strategy:
            standard:   
              sharding-column: order_id   #分片键。对id进行分表
              precise-algorithm-class-name: com.jingai.sharing.jdbc.algorithm.StandardShardingAlgorithm  #精准分片算法

配置方式是在对应的datasource-strategy或table-strategy后面用standard标识标准分片策略。

standard:标识标准分片策略

precise-algorithm-class-name:指定精准分片算法的全路径类名

5.1.3 结果小结

1)精准分片算法根据每个分片键的值,返回分片键所在的表。只处理SQL中分片键含=、IN 的分片处理;

1.1)如果是=的分片,执行一次精准分片算法;

1.2)如果是IN的分片,IN中有几个值,执行几次精准分片算法;

根据返回的最终真实表的个数,分表执行对应SQL操作。同一个真实表的多次操作会进行合并处理,如:IN中传入1、2、3三个数,执行3次的精准分片算法,分别返回tb_order_2、tb_order_1、tb_order_2,则最终执行两条查询。

2)对于非=、IN的分片处理,系统报错;

5.2 范围分片算法

通过实现RangeShardingAlgorithm接口实现范围分片算法,该接口只有一个方法,源码如下:

public interface RangeShardingAlgorithm<T extends Comparable<?>> extends ShardingAlgorithm {

	/**
     * @param availableTargetNames 可用的datasource或table的名称集合
     * @param shardingValue sql中,传入的分片键的值区间
     * @return
     */
    Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<T> shardingValue);
}

通精准分片算法一样,数据库分片和表分片用的同一个接口。此处以表分片为例。

5.2.1 自定义表分片算法

package com.jingai.sharing.jdbc.algorithm;

import com.google.common.collect.Range;
import org.apache.logging.log4j.util.Strings;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.PreciseShardingValue;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.standard.RangeShardingValue;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

public class StandardShardingAlgorithm implements PreciseShardingAlgorithm<Long>, RangeShardingAlgorithm<Long> {
    @Override
    public String doSharding(Collection<String> tableNames, PreciseShardingValue<Long> preciseShardingValue) {
        for (String tableName : tableNames) {
            String mod = preciseShardingValue.getValue() % tableNames.size() + 1 + Strings.EMPTY;
            if(tableName.endsWith(mod)) {
                return tableName;
            }
        }
        throw new IllegalArgumentException("找不到匹配的表分片");
    }

    @Override
    public Collection<String> doSharding(Collection<String> tableNames, RangeShardingValue<Long> rangeShardingValue) {
        Range<Long> valueRange = rangeShardingValue.getValueRange();
        Long lower = valueRange.hasLowerBound() ? valueRange.lowerEndpoint() : null;
        Long upper = valueRange.hasUpperBound() ? valueRange.upperEndpoint() : null;
        // 如果区间不确定,则需要全表操作
        if(lower == null || upper == null) {
            return tableNames;
        }
        Set<String> rs = new HashSet<>();
        // 循环计算需要用到的表
        for (long i = lower; i < upper ; i ++) {
            for(String name : tableNames) {
                String mod = i % tableNames.size() + 1 + Strings.EMPTY;
                if(name.endsWith(mod)) {
                    rs.add(name);
                }
            }
        }
        return rs;

    }
}

以上代码为完整的标准分片策略算法,同时实现了精准分片算法和范围分片算法。算法的逻辑都是按照分片键取模加1。

5.2.2 分片配置

spring:
  shardingsphere:
    datasource:
      names: order1 #数据源名称,有几个数据源就写几个名字,和url中的数据库名字保持一致
      order1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/shardingjdbctest?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
        username: root
        password: 123456
    #分表策略
    #按照id分表,id使用雪花算保证全局唯一,具体算法:tb_order是表前缀,拼接上:$->{id % 2}  的值
    sharding:
      tables:
        tb_order: #逻辑表
          actual-data-nodes: order1.tb_order_$->{1..2}  #order1:数据源名称;两个tb_order表,分别为tb_order_1和tb_order_2
          key-generator: # 指定主键生成策略
            column: order_id
            type: SNOWFLAKE
          table-strategy:
            standard:   # 标准分片策略
              sharding-column: order_id   #分片键。对id进行分表
              precise-algorithm-class-name: com.jingai.sharing.jdbc.algorithm.StandardShardingAlgorithm  #精准分片算法
              range-algorithm-class-name: com.jingai.sharing.jdbc.algorithm.StandardShardingAlgorithm  #范围分片算法

range-algorithm-class-name:指定范围分片算法的全路径类名

5.2.3 结果小结

对于分片键含 >、<、>=、<= 的分片,在范围分片算法中的ValueRange中,lower和upper只有其中一个有值;

复合分片策略

复合分片策略(ComplexShardingStrategy),用于处理使用多键作为分片键进行分片的场景,包含多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。处理分片键中含有 =、IN、BETWEEN AND、>、<、>=、<= 等操作符进行分片的场景。

复合分片策略提供了ComplexKeysShardingAlgorithm接口,通过重写doSharding()方法实现复合分片算法。源码如下:

public interface ComplexKeysShardingAlgorithm<T extends Comparable<?>> extends ShardingAlgorithm {
    
    /**
     * @param availableTargetNames 可用的datasource或table的名称集合
     * @param shardingValue 存放分片键及对应的值集合或值区间
     * @return
     */
    Collection<String> doSharding(Collection<String> availableTargetNames, ComplexKeysShardingValue<T> shardingValue);
}

数据库分片和表分片都是实现同一个接口,此处以表按member_id和order_id分片为例。

6.1 自定义表分片算法

package com.jingai.sharing.jdbc.algorithm;

import com.google.common.collect.Range;
import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingAlgorithm;
import org.apache.shardingsphere.api.sharding.complex.ComplexKeysShardingValue;

import java.util.Collection;
import java.util.HashSet;
import java.util.Map;

public class OrderComplexShardingAlgorithm implements ComplexKeysShardingAlgorithm<Long> {

    @Override
    public Collection<String> doSharding(Collection<String> tableNames, ComplexKeysShardingValue<Long> complexKeysShardingValue) {
        // IN、= 的处理
        Map<String, Collection<Long>> valuesMap = complexKeysShardingValue.getColumnNameAndShardingValuesMap();
        Collection<Long> memberIds = valuesMap.containsKey("member_id") ? valuesMap.get("member_id") : new HashSet<>();
        Collection<Long> orderIds = valuesMap.containsKey("order_id") ? valuesMap.get("order_id") : new HashSet<>();
        // 区间的处理
        Map<String, Range<Long>> rangeMap = complexKeysShardingValue.getColumnNameAndRangeValuesMap();
        if(rangeMap.containsKey("member_id")) {
            memberIds.addAll(getRangValue(rangeMap.get("member_id"), tableNames.size()));
        }
        if(rangeMap.containsKey("order_id")) {
            orderIds.addAll(getRangValue(rangeMap.get("order_id"), tableNames.size()));
        }
        return getTableNames(memberIds, orderIds, tableNames);
    }

    private Collection<Long> getRangValue(Range<Long> range, int tableNameSize) {
        Long lower = range.hasLowerBound() ? range.lowerEndpoint() : null;
        Long upper = range.hasUpperBound() ? range.upperEndpoint() : null;
        Collection<Long> rs = new HashSet<>(tableNameSize);
        if(lower == null || upper == null) {
            lower = 1l;
            upper = (long)tableNameSize;
        } else {
            upper = upper > (lower + tableNameSize) ? (lower + tableNameSize) : upper;
        }
        for (long i = lower ; i <= upper ; i ++) {
            rs.add(i);
        }
        return rs;
    }

    private Collection<String> getTableNames(Collection<Long> memberIds, Collection<Long> orderIds, Collection<String> tableNames) {
        int size = tableNames.size();
        Collection<String> rs = new HashSet<>(size < 16 ? size : 16);
        // 通过对memberId和orderId分别取模进行分库
        size = size / 2; // 两个分片键
        String underline = "_";
        // 如果没有memberId值,则根据orderId扫描,不处理系统会报错
        if(memberIds.isEmpty()) {
            for(long orderId : orderIds) {
                String suffix = underline + orderId % size;
                for(String tableName : tableNames) {
                    if(tableName.endsWith(suffix)) {
                        rs.add(tableName);
                        break;
                    }
                }
            }
        } else if(orderIds.isEmpty()) {
            for(long memberId : memberIds) {
                String middle = underline + memberId % size + underline;
                for(String tableName : tableNames) {
                    if(tableName.contains(middle)) {
                        rs.add(tableName);
                        break;
                    }
                }
            }
        } else {
            for (long memberId : memberIds) {
                for(long orderId : orderIds) {
                    String suffix = memberId % size + "_" + orderId % size;
                    for(String tableName : tableNames) {
                        if(tableName.endsWith(suffix)) {
                            rs.add(tableName);
                            break;
                        }
                    }
                }
            }
        }
        return rs;
    }
}

在ComplexKeysShardingAlgorithm接口中,包含了精准和范围分片的算法。算法为先按member_id取模,然后再按order_id取模。如果只有member_id,没有order_id,则对member_id下的所有表操作;如果只有order_id没有member_id,则对order_id下的所有member的表操作。

注:在自定义分片算法时,对于存在分片键的操作(即进入了自定义分片算法),都需要有返回值,否则系统会报找不到路由信息的异常错误。

6.2 分片配置

# 单数据库,inline分片策略测试
server:
  port: 8080

#sharding-jdbc分片规则配置
spring:
  shardingsphere:
    datasource:
      names: order1 #数据源名称,有几个数据源就写几个名字,和url中的数据库名字保持一致
      order1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/shardingjdbctest?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
        username: root
        password: 123456
    #分表策略
    #按照id分表,id使用雪花算保证全局唯一
    sharding:
      tables:
        tb_order: #逻辑表
          actual-data-nodes: order1.tb_order_$->{0..1}_$->{0..1}  #order1:数据源名称;四个tb_order表,分别为tb_order_0_0到tb_order_1_1
          key-generator: # 指定主键生成策略
            column: order_id
            type: SNOWFLAKE
          table-strategy:
            complex:   # 复合分片策略
              sharding-columns: member_id, order_id   #分片键
              algorithm-class-name: com.jingai.sharing.jdbc.algorithm.OrderComplexShardingAlgorithm  #分片算法
    props:
      sql:
        show: true  # 是否打印sql

配置方式是在对应的datasource-strategy或table-strategy后面用complex标识复合分片策略。

complex:表示复合分片策略

sharding-columns指定复合分片算法的分片键,多个中间用","隔开

algorithm-class-name指定复合分片算法的全路径类名

Hint分片策略

Hint分片策略(HintShardingStrategy),用于处理使用 Hint 行分片的场景。相比另外三种策略,该分片策略无需配置分片键,而是在执行数据库操作之前,进行分片信息的设定,使得数据库操作指定的分库、分表中执行。

Hint分片策略通过Hint API实现指定操作,使得分片规则变成个性化配置。

Hint分片策略提供了HintShardingAlgorithm接口,实现自定义Hint分片算法。源码如下:

public interface HintShardingAlgorithm<T extends Comparable<?>> extends ShardingAlgorithm {

    /**
     * @param availableTargetNames 可用的datasource或table的名称集合
     * @param shardingValue 分片键及设置的分片键的值
     * @return
     */
    Collection<String> doSharding(Collection<String> availableTargetNames, HintShardingValue<T> shardingValue);
}

数据库分片和表分片都是实现同一个接口,此处以表按member_id和order_id分片为例。

7.1 自定义表分片算法

public class OrderHintShardingAlgorithm implements HintShardingAlgorithm<Integer> {
    @Override
    public Collection<String> doSharding(Collection<String> tableNames, HintShardingValue<Integer> hintShardingValue) {
        Collection<String> rs = new HashSet<>();
        for(Integer val : hintShardingValue.getValues()) {
            String suffix = "_" + (val % tableNames.size() + 1);
            for(String tableName : tableNames) {
                if(tableName.endsWith(suffix)) {
                    rs.add(tableName);
                    break;
                }
            }
        }
        return rs.isEmpty() ? tableNames : rs;
    }
}

算法比较简单,以设置的分片键的值取模。

7.2 分片配置

# 单数据库,inline分片策略测试
server:
  port: 8080

#sharding-jdbc分片规则配置
spring:
  shardingsphere:
    datasource:
      names: order1 #数据源名称,有几个数据源就写几个名字,和url中的数据库名字保持一致
      order1:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/shardingjdbctest?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
        username: root
        password: 123456
    #分表策略
    #按照id分表,id使用雪花算保证全局唯一,具体算法:tb_order是表前缀,拼接上:$->{id % 2}  的值
    sharding:
      tables:
        tb_order: #逻辑表
          actual-data-nodes: order1.tb_order_$->{1..2}  #order1:数据源名称;两个tb_order表,分别为tb_order_1和tb_order_2
          key-generator: # 指定主键生成策略
            column: order_id
            type: SNOWFLAKE
          table-strategy:
            hint:  # hint分片策略
              algorithm-class-name: com.jingai.sharing.jdbc.algorithm.OrderHintShardingAlgorithm  #分片算法
    props:
      sql:
        show: true  # 是否打印sql

7.3 方法

@RestController
public class OrderController {

    @Resource
    private OrderService orderService;

    @RequestMapping("order")
    public String order(OrderEntity order) {
        // 清除上一次的规则
        HintManager.clear();
        // 指定插入到1的表
        HintManager hintManager = HintManager.getInstance();
        hintManager.addTableShardingValue("tb_order", 1);
        order.setOrderTime(new Date());
        long insert = orderService.insert2(order);
        return insert > 0 ? "success" : "fail";
    }
}

在执行插入之前,通过HintManager的addTableShardingValue()指定如果是逻辑表为tb_order,则存放在分片键值为1的小表中,结合上面的算法,存放在tb_order_2表中。无论执行多少次这个方法,数据都是存放在tb_order_2这个表。可以执行多次addTableShardingValue()方法,添加多个分片键值。

HintManager在使用之前都需要先执行HintManager.clear(),因为设置的信息是保存在ThreadLocal,可能会冲突。

addDatabaseShardingValue():添加数据库分片键值;

addTableShardingValue():添加表分片键值;

setMasterRouteOnly():在读写分离的数据库中,强制从主库中读取。在最新的5.5.0版本,该方法被setWriteRouteOnly()取代;

小结

以上为本篇分享的全部内容。以下做个小结:

1)行表达式分片策略只适合分片键中包含=、IN的分片处理,无需自定义分片算法,只支持单分片键;

2)标准分片策略适合分片键中包含 =、IN、BETWEEN AND、>、<、>=、<=的分片处理,需自定义分片算法,只支持单分片键;

3)复合分片策略适合分片键中包含 =、IN、BETWEEN AND、>、<、>=、<=的分片处理,需自定义分片算法,支持多个分片键;

4)Hint分片策略适合所有操作的分片处理,需自定义分片算法,可在每次执行前通过HintManager个性化设置分片键值;

5)对于存在分片键的数据库操作,如果没有返回对应的分片库或分片表,则系统会提示找不到路由信息的异常;

以上的示例的完整代码可以结合

Sharding-JDBC分库分表的基本使用-CSDN博客

博文中的示例,是在此基础上修改了配置文件。

说明:以上分片策略算法只是为了演示功能,实际项目的分片需要根据业务需要进行设计。特别是复合分片策略的算法,在实际项目中,应该没人会如此设计。

关于本篇内容你有什么自己的想法或独到见解,欢迎在评论区一起交流探讨下吧。

  • 10
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值