02、Sharding-JDBC 实战:四种分片策略

准备工作

1.SQL

-- ------------------------------
-- 用户表
-- ------------------------------
CREATE TABLE t_user (
  id bigint(16) NOT NULL AUTO_INCREMENT COMMENT '主键',
  username varchar(64) NOT NULL COMMENT '用户名',
  password varchar(64) NOT NULL COMMENT '密码',
  age int(8) NOT NULL COMMENT '年龄',
  salary int(8) NOT NULL COMMENT '工资',
  create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';

-- ------------------------------
-- 用户表1
-- ------------------------------
CREATE TABLE t_user_1 (
  id bigint(16) NOT NULL AUTO_INCREMENT COMMENT '主键',
  username varchar(64) NOT NULL COMMENT '用户名',
  password varchar(64) NOT NULL COMMENT '密码',
  age int(8) NOT NULL COMMENT '年龄',
  salary int(8) NOT NULL COMMENT '工资',
  create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表1';

-- ------------------------------
-- 用户表2
-- ------------------------------
CREATE TABLE t_user_2 (
  id bigint(16) NOT NULL AUTO_INCREMENT COMMENT '主键',
  username varchar(64) NOT NULL COMMENT '用户名',
  password varchar(64) NOT NULL COMMENT '密码',
  age int(8) NOT NULL COMMENT '年龄',
  salary int(8) NOT NULL COMMENT '工资',
  create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  update_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表2';

2.数据源配置

注意:数据库命名不能使用下划线,会报错:mydb_0 invalid

spring:
  shardingsphere:
    打印sql
    props:
      sql:
        show: true
    datasource:
      names: mydb-1,mydb-2
      mydb-1:
        type: com.alibaba.druid.pool.DruidDataSource
        url: jdbc:mysql://localhost:3306/mydb-1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
        driver-class-name: com.mysql.cj.jdbc.Driver
        username: root
        password: root
        数据源其他配置
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
        配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
       filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
      mydb-2:
        type: com.alibaba.druid.pool.DruidDataSource
        url: jdbc:mysql://localhost:3306/mydb-2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
        driver-class-name: com.mysql.cj.jdbc.Driver
        username: root
        password: root
        数据源其他配置
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
        配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
       filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

一、标准分片策略

标准分片策略(StandardShardingStrategy): 只支持对单个分片键为依据的分库分表,并提供了两种分片算法:

  • PreciseShardingAlgorithm(精准分片):在使用标准分片策略时,精准分片算法时必须实现的算法,用于SQL含有 = 和 IN 的分片处理;
  • RangeShardingAlgorithm(范围分片):非必选的,用于处理含有 BETWEEN AND 的分片处理。

注意: 一旦我们没配置范围分片算法,而SQL中又用到 BETWEEN AND 或者 LIKE 等,那么 SQL 将按全库、表路由的方式逐一执行,查询性能会很差。

使用方法:

使用四种分片策略的方式大致相同,都要实现相应的 ShardingAlgorithm 接口,并重写 doSharding() 方法,只是配置稍有不同,doSharding() 方法本身只是个空方法,需要我们自行处理分库、分表逻辑。

1.精准分片算法

使用场景:

  • SQL 语句中有 >,>=,<=,<,=,IN 和 BETWEEN AND 操作符,都可以应用此分片策略。
select * from t_user where age = 10 or age in (10,11,12);

1.1 精准分库算法

yaml配置:

spring:
  shardingsphere:
    sharding:
      默认数据源,未分片的表默认执行库
      default-data-source-name: mydb-1
      表策略配置
      tables:
        t_user 是逻辑表
        t_user:
          databaseStrategy:
            standard:
              分片键
              shardingColumn: age
              精准分库算法
              preciseAlgorithmClassName: com.demo.module.config.MyDBPreciseShardingAlgorithm

代码实现:

实现PreciseShardingAlgorithm 接口,并重写 doSharding()。

public class MyDBPreciseShardingAlgorithm implements PreciseShardingAlgorithm<Integer> {
   
     

    @Override
    public String doSharding(Collection<String> databaseNames, PreciseShardingValue<Integer> shardingValue) {
   
     

        /**
         * databaseNames 所有分片库的集合
         * shardingValue 为分片属性,其中 logicTableName 为逻辑表,columnName 分片键,value 为从 SQL 中解析出的分片键的值
         */
        for (String databaseName : databaseNames) {
   
     
            // 根据年龄判断,未成年 -> mydb-1, 已成年 -> mydb-2
            String value = String.valueOf(shardingValue.getValue() < 18 ? 1 : 2);
            if (databaseName.endsWith(value)) {
   
     
                return databaseName;
            }
        }
        throw new IllegalArgumentException("分片失败,databaseNames:" + databaseNames);
    }
}

1.2 精准分表算法

yaml配置:

spring:
  shardingsphere:
    sharding:
      默认数据源,未分片的表默认执行库
      default-data-source-name: mydb-1
      表策略配置
      tables:
        t_user 是逻辑表
        t_user:
          分表节点 可以理解为分表后的那些表 比如 t_user_1 ,t_user_2
          actualDataNodes: mydb-1.t_user_$->{
   
     1..2}
          tableStrategy:
            standard:
              分片键
              shardingColumn: age
              精准分表算法
              preciseAlgorithmClassName: com.demo.module.config.MyTablePreciseShardingAlgorithm

代码实现:

实现PreciseShardingAlgorithm 接口,并重写 doSharding()。

public class MyTablePreciseShardingAlgorithm implements PreciseShardingAlgorithm<Integer> {
   
     

    @Override
    public String doSharding(Collection<String> tableNames, PreciseShardingValue<Integer> shardingValue) {
   
     
        /**
         * tableNames 对应分片库中所有分片表的集合
         * shardingValue 为分片属性,其中 logicTableName 为逻辑表,columnName 分片键,value 为从 SQL 中解析出来的分片键的值
         */
        for (String tableName : tableNames) {
   
     
            // 取模算法,分片键 % 表数量 + 1
            String value = String.valueOf(shardingValue.getValue() % tableNames.size() + 1);
            if (tableName.endsWith(value)) {
   
     
                return tableName;
            }
        }
        throw new IllegalArgumentException("分片失败,tableNames:" + tableNames);
    }
}

2.范围分片算法

使用场景:

  • 当 SQL 中的分片键用到 BETWEEN AND 操作符会使用到此算法,会根据 SQL 中给出的分片键范围值处理分库分表逻辑。
select * from t_user where age between 10 and 12;

yaml配置:

spring:
  shardingsphere:
    sharding:
      默认数据源,未分片的表默认执行库
      default-data-source-name: mydb-1
      表策略配置
      tables:
        t_user 是逻辑表
        t_user:
          databaseStrategy:
            standard:
              分片键
              shardingColumn: age
              精准分库算法
              preciseAlgorithmClassName: com.demo.module.config.MyDBPreciseShardingAlgorithm
              范围分库算法
              rangeAlgorithmClassName: com.demo.module.config.MyDBRangeShardingAlgorithm

代码实现:

实现PreciseShardingAlgorithm 接口,并重写 doSharding()。

public class MyDBRangeShardingAlgorithm implements RangeShardingAlgorithm<Integer> {
   
     

    /**
     * 数据库分片符号
     */
    private final String DATABASE_SPLIT_SYMBOL = "-";

    @Override
    public Collection<String> doSharding(Collection<String> databaseNames, RangeShardingValue<Integer> rangeShardingValue) {
   
     
        Set<String> result = new LinkedHashSet<>();

        // between and 的起始值
        Range<Integer> valueRange = rangeShardingValue.getValueRange();
        boolean hasLowerBound = valueRange.hasLowerBound();
        boolean hasUpperBound = valueRange.hasUpperBound();

        // 计算最大值和最小值(未成年 -> mydb-1, 已成年 -> mydb-2)
        int lower;
        if (hasLowerBound) {
   
     
            lower = valueRange.lowerEndpoint() < 18 ? 1 : 2;
        } else {
   
     
            lower = getLowerEndpoint(databaseNames);
        }
        int upper;
        if (hasUpperBound) {
   
     
            upper = valueRange.upperEndpoint() < 18 ? 1 : 2;
        } else {
   
     
            upper = getUpperEndpoint(databaseNames);
        }

        // 循环范围计算分库逻辑
        for (int i = lower; i <= upper; i++) {
   
     
            for (String databaseName : databaseNames) {
   
     
                String value = String.valueOf(i);
                if (databaseName.endsWith(value)) {
   
     
                    result.add(databaseName);
                }
            }
        }
        return result;
    }

    // --------------------------------------------------------------------------------------------------------------
    // 私有方法
    // --------------------------------------------------------------------------------------------------------------

    /**
     * 获取 最小分片值
     * @param databaseNames 数据库名
     * @return 最小分片值
     */
    private int getLowerEndpoint(Collection<String> databaseNames) {
   
     
        if (CollectionUtils.isNotEmpty(databaseNames)) {
   
     
            return databaseNames.stream().filter(o -> o != null && o.contains(DATABASE_SPLIT_SYMBOL))
                    .mapToInt(o -> {
   
     
                        String[] splits = o.split(DATABASE_SPLIT_SYMBOL);
                        return Integer.valueOf(splits[splits.length - 1]);
                    }).min().orElse(-1);
        }
        return -1;
    }

    /**
     * 获取 最大分片值
     * @param databaseNames 数据库名
     * @return 最大分片值
     */
    private int getUpperEndpoint(Collection<String> databaseNames) {
   
     
        if (CollectionUtils.isNotEmpty(databaseNames)) {
   
     
            return databaseNames.stream().filter(o -> o != null && o.contains(DATABASE_SPLIT_SYMBOL))
                    .mapToInt(o -> {
   
     
                        String[] splits = o.split(DATABASE_SPLIT_SYMBOL);
                        return Integer.valueOf(splits[splits.length - 1]);
                    }).max().orElse(-1);
        }
        return -1;
    }
}

测试验证1-插入数据:

    @Test
    void saveTest() {
   
     
        List<TUser> users = new ArrayList<>(3);
        users.add(new TUser("ACGkaka_1", "123456", 10, 5000));
        users.add(new TUser("ACGkaka_2", "123456", 15, 6000));
        users.add(new TUser("ACGkaka_3", "123456", 18, 7000));
        userService.saveBatch(users);
    }

操作结果:

 

 

测试验证2-查询数据:

    @Test
    void listTest() {
   
     
        QueryWrapper<TUser> wrapper = new QueryWrapper<>();
        wrapper.ge("age", 15);
        List<TUser> users = userService.list(wrapper);
        System.out.println(">>>>>>>>>> 【Result】 <<<<<<<<<< ");
        users.forEach(System.out::println);
    }

操作结果:

 

二、复合分片策略

使用场景:

  • SQL语言中有 >,>=,<=,<,=,IN 和 BETWEEN AND 等操作符,不同的是复合分片策略支持对多个分片键操作。
select * from t_user where age=10 and username='ACGkaka_1';

yaml配置:

spring:
  shardingsphere:
    sharding:
      表策略配置
      tables:
        t_user 是逻辑表
        t_user:
          databaseStrategy:
            standard:
              分片键
              shardingColumn: age
              精准分库算法
              preciseAlgorithmClassName: com.demo.module.config.MyDBPreciseShardingAlgorithm
              范围分库算法
              rangeAlgorithmClassName: com.demo.module.config.MyDBRangeShardingAlgorithm

代码实现:

public class MyDBComplexKeysShardingAlgorithm implements ComplexKeysShardingAlgorithm<Integer> {
   
     

    /**
     * 复合分库
     *
     * @param databaseNames 全部库名集合(用于筛选)
     * @param complexKeysShardingValue 复合分片值
     * @return 需要用到的库名集合
     */
    @Override
    public Collection<String> doSharding(Collection<String> databaseNames, ComplexKeysShardingValue<Integer> complexKeysShardingValue) {
   
     

        // 精准分片-得到每个分片键对应的值
        Collection<Integer> ageValues = this.getPreciseShardingValue(complexKeysShardingValue, "age");
        Collection<Integer> salaryValues = this.getPreciseShardingValue(complexKeysShardingValue, "salary");

        // 精准分片-根据两个分片键进行分库
        List<String> ageSuffix = ageValues.stream().map(o -> o < 18 ? "1" : "2").collect(Collectors.toList());
        List<String> salarySuffix = salaryValues.stream().map(o -> o < 5000 ? "1" : "2").collect(Collectors.toList());
        List<String> shardingSuffix = getShardingSuffix(ageSuffix, salarySuffix);

        // 范围分片-得到每个分片键对应的值
        Range<Integer> ageRange = this.getRangeShardingValue(complexKeysShardingValue, "age");
        Range<Integer> salaryRange = this.getRangeShardingValue(complexKeysShardingValue, "salary");

        // 范围分片-根据两个分片键进行分库
        ageSuffix = getRangeSuffix(ageRange);
        salarySuffix = getRangeSuffix(salaryRange);
        shardingSuffix.addAll(getShardingSuffix(ageSuffix, salarySuffix));

        return shardingSuffix.stream().distinct().collect(Collectors.toList());
    }
    // --------------------------------------------------------------------------------------------------------------
    // 私有方法
    // --------------------------------------------------------------------------------------------------------------
    /**
     * 获取 分库的范围
     * @param range 分片值范围
     * @return 分库范围
     */
    private List<String> getRangeSuffix(Range<Integer> range) {
   
     
        boolean isValid = range != null && (range.hasLowerBound() || range.hasUpperBound());
        if (isValid) {
   
     
            List<String> rangeSuffix = new ArrayList<>();
            int lowerEndpoint = range.hasLowerBound() ? range.lowerEndpoint() < 18 ? 1 : 2 : 1;
            int upperEndpoint = range.hasUpperBound() ? range.upperEndpoint() < 18 ? 1 : 2 : 2;
            for (int i = lowerEndpoint; i <= upperEndpoint; i++) {
   
     
                rangeSuffix.add(String.valueOf(i));
            }
            return rangeSuffix;
        }
        return new ArrayList<>(0);
    }

    /**
     * 获取 库名集合
     * @param ageSuffix age分片值
     * @param salarySuffix salary分片值
     * @return 库名集合
     */
    private List<String> getShardingSuffix(List<String> ageSuffix, List<String> salarySuffix) {
   
     
        List<String> dbNames = new ArrayList<>();
        ageSuffix = ageSuffix == null ? new ArrayList<>(0) : ageSuffix;
        salarySuffix = salarySuffix == null ? new ArrayList<>(0) : salarySuffix;
        for (String age : ageSuffix) {
   
     
            for (String salary : salarySuffix) {
   
     
                dbNames.add(String.format("mydb-%s-%s", age, salary));
            }
        }
        return dbNames.stream().distinct().collect(Collectors.toList());
    }

    /**
     * 获取 分片键对应的值
     *
     * @param shardingValue 复合分片值
     * @param key 分片键
     * @return 值
     */
    private Collection<Integer> getPreciseShardingValue(ComplexKeysShardingValue<Integer> shardingValue, final String key) {
   
     
        // 判断非空
        if (StringUtils.isEmpty(key) || shardingValue == null) {
   
     
            return new ArrayList<>(0);
        }

        // 根据 分片键 取值
        Collection<Integer> valueSet = new ArrayList<>();
        Map<String, Collection<Integer>> columnNameAndShardingValueMap = shardingValue.getColumnNameAndShardingValuesMap();
        Map<String, String> columnNameMap = columnNameAndShardingValueMap.keySet().stream().filter(Objects::nonNull)
                .collect(Collectors.toMap(String::toUpperCase, o -> o, (k1, k2) -> k1));
        if (columnNameMap.containsKey(key.toUpperCase())) {
   
     
            String columnName = columnNameMap.get(key.toUpperCase());
            valueSet.addAll(columnNameAndShardingValueMap.get(columnName));
        }
        return valueSet;
    }

    /**
     * 获取 分片键对应的值
     *
     * @param shardingValue 复合分片值
     * @param key 分片键
     * @return 值
     */
    private Range<Integer> getRangeShardingValue(ComplexKeysShardingValue<Integer> shardingValue, final String key) {
   
     
        // 判断非空
        if (StringUtils.isEmpty(key) || shardingValue == null) {
   
     
            return null;
        }

        // 根据 分片键 取值
        Map<String, Range<Integer>> columnNameAndRangeValuesMap = shardingValue.getColumnNameAndRangeValuesMap();
        Map<String, String> columnNameMap = columnNameAndRangeValuesMap.keySet().stream().filter(Objects::nonNull)
                .collect(Collectors.toMap(String::toUpperCase, o -> o, (k1, k2) -> k1));
        if (columnNameMap.containsKey(key.toUpperCase())) {
   
     
            String columnName = columnNameMap.get(key.toUpperCase());
            return columnNameAndRangeValuesMap.get(columnName);
        } else {
   
     
            return null;
        }
    }
}

测试验证1-插入数据:

    @Test
    void saveTest() {
   
     
        List<TUser> users = new ArrayList<>(3);
        users.add(new TUser("ACGkaka_1", "123456", 10, 3000));
        users.add(new TUser("ACGkaka_2", "123456", 18, 4000));
        users.add(new TUser("ACGkaka_3", "123456", 15, 6000));
        users.add(new TUser("ACGkaka_4", "123456", 19, 7000));
        userService.saveBatch(users);
    }

操作结果:

 

 

 

 

测试验证2-查询数据:

    @Test
    void listTest() {
   
     
        QueryWrapper<TUser> wrapper = new QueryWrapper<>();
        wrapper.ge("age", 18);
        wrapper.ge("salary", 5000);
        List<TUser> users = userService.list(wrapper);
        System.out.println(">>>>>>>>>> 【Result】 <<<<<<<<<< ");
        users.forEach(System.out::println);
    }

操作结果:

 

三、行表达式分片策略

**行表达式分片策略(InlineShardingStrategy):**在配置中使用 Groovy 表达式,提供对 SQL 语言中的 = 和 IN 的分片操作支持,它只支持单分片键。

使用场景:

  • 适用于做简单的分片算法,无需自定义分片算法,省去了繁琐的代码开发,是四种分片策略中最为简单的。

比如:ds-KaTeX parse error: Expected '}', got 'EOF' at end of input: … 表示对 age 做取模计算, 是个通配符,用来承接计算结果,最终计算出分库 ds-0 … ds-n

yaml配置:

spring:
  shardingsphere:
    sharding:
      表策略配置
      tables:
        t_user 是逻辑表
        t_user:
          分表节点 可以理解为分表后的那些表 比如 t_user_1 ,t_user_2 ,t_user_3
          actualDataNodes: mydb.t_user_$->{
   
     1..3}
          tableStrategy:
            inline:
              根据哪列分表
              shardingColumn: age
              分表算法 例如:age为奇数 -> t_user_2; age为偶数 -> t_user_1
              algorithmExpression: t_user_$->{
   
     age % 2 + 1}

四、Hint分片策略

Hint分片策略(HintShardingStrategy): 相比于其他分片策略稍有不同,这种分片策略无需配置分片键,分片键值也不再从 SQL 中解析,而是由外部指定分片信息,让 SQL 在指定的分库、分表中执行。

  • ShardingSphere 通过 Hint API 实现指定操作,实际上就是把分片规则 tablerule、databaserule 由集中配置变成了个性化配置。

使用场景:

  • 如果我们希望用户表 t_user 用 age 做分片键进行分库分表,但是 t_user 表中却没有 age 这个字段,这时可以通过 Hint API 在外部手动指定分片键或分片库。

yaml配置:

spring:
  shardingsphere:
    sharding:
      表策略配置
      tables:
        t_user 是逻辑表
        t_user:
          分表节点 可以理解为分表后的那些表 比如 t_user_1 ,t_user_2
          actualDataNodes: mydb.t_user_$->{
   
     1..2}
          tableStrategy:
            hint:
              复合分库算法
              algorithmClassName: com.demo.module.config.MyTableHintShardingAlgorithm

代码实现:

public class MyTableHintShardingAlgorithm implements HintShardingAlgorithm<Integer> {
   
     

    @Override
    public Collection<String> doSharding(Collection<String> tableNames, HintShardingValue<Integer> hintShardingValue) {
   
     

        List<String> result = new ArrayList<>();
        for (Integer shardingValue : hintShardingValue.getValues()) {
   
     
            result.add(shardingValue < 18 ? "t_user_1" : "t_user_2");
        }
        return result;
    }
}

测试验证1-插入数据:

    @Test
    void hintSaveTest() {
   
     
        // 清除掉上一次的规则,否则会报错
        HintManager.clear();
        // HintManager API 工具类实例
        HintManager hintManager = HintManager.getInstance();
        // 直接指定对应具体的数据库
        hintManager.addDatabaseShardingValue("mydb",0);
        // 设置表的分片键值,自定义操作哪个分片中
        hintManager.addTableShardingValue("t_user" , 18);

        // 在读写分离数据库中,Hint 可以强制读主库
        hintManager.setMasterRouteOnly();

        List<TUser> users = new ArrayList<>(3);
        users.add(new TUser("ACGkaka_1", "123456", 10, 3000));
        users.add(new TUser("ACGkaka_2", "123456", 18, 4000));
        users.add(new TUser("ACGkaka_3", "123456", 15, 6000));
        users.add(new TUser("ACGkaka_4", "123456", 19, 7000));
        userService.saveBatch(users);
    }

操作结果:

 

测试验证2-查询数据:

    @Test
    void hintListTest() {
   
     
        // 清除掉上一次的规则,否则会报错
        HintManager.clear();
        // HintManager API 工具类实例
        HintManager hintManager = HintManager.getInstance();
        // 直接指定对应具体的数据库
        hintManager.addDatabaseShardingValue("mydb",0);
        // 设置表的分片键值,自定义操作哪个分片中
        hintManager.addTableShardingValue("t_user" , 18);

        // 在读写分离数据库中,Hint 可以强制读主库
        hintManager.setMasterRouteOnly();

        QueryWrapper<TUser> wrapper = new QueryWrapper<>();
        List<TUser> users = userService.list(wrapper);
        System.out.println(">>>>>>>>>> 【Result】 <<<<<<<<<< ");
        users.forEach(System.out::println);
    }

操作结果:

 

五、源码地址

地址: https://gitee.com/acgkaka/SpringBootExamples/tree/master/springboot-sharding-jdbc-4types

参考地址:

1、 分库分表的4种分片策略,所有SQL都逃不掉的一步,https://blog.51cto.com/u_14787961/3200290;