ShardingJdbc 分库分表 读写分离

目录

1、核心概念

1.1逻辑表

1.2真实表

1.3数据节点

2、数据分片

2.1分片键

2.2分片算法

2.3分片策略

3.1核心概念

3.2核心功能

3.3不支持项

4、项目实施

4.1业务介绍

4.2代码实施

5、实施验证

5.1验证未配置分片规则的表将走默认数据源

5.2验证配置分片规则的表将走分库分表


1、核心概念

1.1逻辑表

水平拆分的数据库(表)的相同逻辑和数据结构表的总称如温度数据表,根据上传的日期,一月可以分为31张表,temp_1.....temp_31,它们的逻辑表名就是temp。

配置文件对应信息:spring.shardingsphere.sharding.tables.temp

1.2真实表

在分片的数据库中,真实存在的表,如上temp_1......temp_31

1.3数据节点

数据分片的最小单元由数据源名称和数据表组成如ds202110.temp1

注意:ds202110是配置文件中读写分离配置规则(master-slave-rules)的名称,在该名称下需要配置写库及读库

配置文件对应信息:

spring.shardingsphere.sharding.tables.actual-data-nodes

2、数据分片

2.1分片键

用于分片的数据库字段,是将数据库(表)水平拆分的关键字段。

例如:将温度表中上传时间temppoint,取yyyy-MM-dd中的天对表进行分片,年月对库进行分片,SQL中如果无分片字段,将执行全路由,性能较差。 除了对单分片字段的支持,ShardingSphere也支持根据多个字段进行分片(多个分片键在配置文件中用逗号分割),需要采用复合分片算法,并在开发中进行代码实现。

2.2分片算法

通过分片算法将数据分片,支持通过=、INBETWEEN AND分片分片算法需要应用方开发者自行实现

2.2.1精确分片算法

对应PreciseShardingAlgorithm接口,用于处理使用单一键作为分片键的=与IN进行分片的场景,在开发过程中,如果使用标准分片策略,需要程序员实现此接口,重写doSharding()方法。

2.2.2范围分片算法

对应RangeShardingAlgorithm接口,用于处理使用单一键作为分片键的BETWEEN AND进行分片的场景,在开发过程中如若使用标准分片策略且代码中根据分片键进行范围查询用到了Between And,需要程序员实现此接口,重写doSharding()方法。

2.2.3复合分片算法

对应ComplexKeysShardingAlgorithm接口,用于处理使用多键(多个数据库表字段)作为分片键进行分片的场景,提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持,包含多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。在开发过程中如果使用复合分片策略,需要程序员实现此接口,重写doSharding()方法。

2.2.4Hint分片算法

对应HintShardingAlgorithm接口,用于处理使用Hint行分片的场景。Hint 分片算法(HintShardingAlgorithm)稍有不同,上边的算法中我们都是解析 SQL 语句提取分片键,并设置分片策略进行分片。但有些时候我们并没有使用任何的分片键和分片策略,可还想将 SQL 路由到目标数据库和表,就需要通过手动干预指定 SQL 的目标数据库和表信息,这也叫强制路由。

2.3分片策略

包含分片键和分片算法,由于分片算法的独立性,将其独立抽离。真正可用于分片操作的是分片键 + 分片算法,也就是分片策略。目前官网提供5种分片策略。在配置文件中对应的配置关键词是:standard、complex、inline、hint

2.3.1标准分片策略

对应StandardShardingStrategy。提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持。 StandardShardingStrategy只支持单分片键,提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。PreciseShardingAlgorithm是必选的,用于处理=和IN的分片。RangeShardingAlgorithm是可选的,用于处理BETWEEN AND分片,如果不配置RangeShardingAlgorithm,SQL中的BETWEEN AND将按照全库路由处理。

开发中如果配置了标准分片策略,则需要实现精准分片算法接口,并在配置文件中配置,若sql中使用between and,则需要同时实现范围分片算法接口,并配置到配置文件中,如下图所示:

2.3.2复合分片策略

对应ComplexShardingStrategy。复合分片策略。提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持。ComplexShardingStrategy支持多分片键,由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。

配置文件如下所示:通过订单id,及用户id

### 分库策略

# order_id,user_id 同时作为分库分片健

spring.shardingsphere.sharding.tables.t_order.database-strategy.complex.

sharding-column=order_id,user_id

# 复合分片算法

spring.shardingsphere.sharding.tables.t_order.database-strategy.complex.

algorithm-class-name=com.xxShardingAlgorithm

### 分策略

# order_id,user_id 同时作为分分片健

spring.shardingsphere.sharding.tables.t_order.table-strategy.complex.

sharding-column=order_id,user_id

# 复合分片算法

spring.shardingsphere.sharding.tables.t_order.table-strategy.complex.

algorithm-class-name=com.xxShardingAlgorithm

2.3.3行表达式分片策略

对应InlineShardingStrategy。使用Groovy的表达式,提供对SQL语句中的=和IN的分片操作支持,只支持单分片键。对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发,如: t_user_$->{u_id % 8} 表示t_user表根据u_id模8,而分成8张表,表名称为t_user_0到t_user_7。

##分表策略(分库策略同理)行表达式分片键

sharding.jdbc.config.sharding.tables.t_order.database-strategy.inline.

sharding-column=order_id

# 表达式算法

sharding.jdbc.config.sharding.tables.t_order.database-strategy.inline.

algorithm-expression=ds-$->{order_id % 2}

2.3.4Hint分片策略

对应HintShardingStrategy。通过Hint而非SQL解析的方式分片的策略

3、读写分离

面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。 对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。

通过一主多从的配置方式,可以将查询请求均匀的分散到多个数据副本,能够进一步的提升系统的处理能力。 使用多主多从的方式,不但能够提升系统的吞吐量,还能够提升系统的可用性,可以达到在任何一个数据库宕机,甚至磁盘物理损坏的情况下仍然不影响系统的正常运行。

与将数据根据分片键打散至各个数据节点的水平分片不同,读写分离则是根据SQL语义的分析,将读操作和写操作分别路由至主库与从库。

3.1核心概念

主库:添加、更新以及删除数据操作所使用的数据库,目前仅支持单主库

从库:查询数据操作所使用的数据库,可支持多从库

主从同步:将主库的数据异步的同步到从库的操作。由于主从同步的异步性,从库与主库的数据会短时间内不一致。

负载均衡策略:通过负载均衡策略将查询请求疏导至不同从库,默认是随机ROUND_ROBIN,可配置轮询RANDOM

如下所示:

3.2核心功能

  1. 提供一主多从的读写分离配置,可独立使用,也可配合分库分表使用。
  2. 独立使用读写分离支持SQL透传。
  3. 同一线程且同一数据库连接内,如有写入操作,以后的读操作均

从主库读取,用于保证数据一致性。

  1. 基于Hint的强制主库路由。

3.3不支持项

  1. 主库和从库的数据同步。
  2. 主库和从库的数据同步延迟导致的数据不一致。
  3. 主库双写或多写。

根据不支持项,在实际操作中需要配置主从同步策略,在项目配置文件中如果使用了shardingjdbc,则只能配置一个默认主库

4、项目实施

4.1业务介绍

数据情况:

温度数据是支撑项目业务正常运行的基石,按单用户7*24小时的最大量计算,用户每5分钟上传一次数据,则单用户日数据量为:1*24*12=288条,1W用户则为288W条。

分库表策略:

温度数据表temp,根据用户上传数据的时间timepoint,按照月进行分库,日进行分表的策略进行分库分表,则每月一个库,每个库中有31张表,如ds_202110.temp_(1-31)

如果后期用户量增加,可按照上传时间再次细分,如按照月进行分库,按照小时进行分表,则每月1个库,每个库中有62张表,如ds_202110.temp_(1-31)_(00,12),即把一天的数据量分成两个表,一个从00至12,一个12至24点

数据分片策略:

按照标准分片策略进行库及表的分片,在项目业务实现中,会用到IN,=,Between And

4.2代码实施

4.2.1引入依赖

在health-cosunter中引入sharding-jdbc的jar包

<!--分库分表-->

        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
            <version>4.0.0-RC1</version>
        </dependency>

4.2.2引入配置文件

# 读写分离 分库分表

  spring:

    main:

      #允许覆盖

      allow-bean-definition-overriding: true

    shardingsphere:

      props:

        # 开启SQL显示,默认false

        sql:

          show: true

      datasource:

        names: master,slave,cosunter-master-202112,cosunter-slave-202112,cosunter-master-202110,cosunter-slave-202110,cosunter-master-202111,cosunter-slave-202111

        master:

          type: com.alibaba.druid.pool.DruidDataSource

          driver-class-name: com.mysql.cj.jdbc.Driver

          url: jdbc:mysql://192.168.1.228:3306/cosunter    useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=GMT%2b8

          username: root

          password: IuZMLuPGqf0RR3Vh

        slave:

          type: com.alibaba.druid.pool.DruidDataSource

          driver-class-name: com.mysql.cj.jdbc.Driver

          url: jdbc:mysql://192.168.1.228:3306/cosunter_slave?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=GMT%2b8

          username: root

          password: IuZMLuPGqf0RR3Vh

        cosunter-master-202110:

          type: com.alibaba.druid.pool.DruidDataSource

          driver-class-name: com.mysql.cj.jdbc.Driver

          url: jdbc:mysql://192.168.1.228:3306/cosunter_202110?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=GMT%2b8

          username: root

          password: IuZMLuPGqf0RR3Vh

        cosunter-slave-202110:

          type: com.alibaba.druid.pool.DruidDataSource

          driver-class-name: com.mysql.cj.jdbc.Driver

          url: jdbc:mysql://192.168.1.228:3306/cosunter_202110?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=GMT%2b8

          username: root

          password: IuZMLuPGqf0RR3Vh

        cosunter-master-202111:

          type: com.alibaba.druid.pool.DruidDataSource

          driver-class-name: com.mysql.cj.jdbc.Driver

          url: jdbc:mysql://192.168.1.228:3306/cosunter_202111?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=GMT%2b8

          username: root

          password: IuZMLuPGqf0RR3Vh

        cosunter-slave-202111:

          type: com.alibaba.druid.pool.DruidDataSource

          driver-class-name: com.mysql.cj.jdbc.Driver

          url: jdbc:mysql://192.168.1.228:3306/cosunter_202111?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=GMT%2b8

          username: root

          password: IuZMLuPGqf0RR3Vh

        cosunter-master-202112:

          type: com.alibaba.druid.pool.DruidDataSource

          driver-class-name: com.mysql.cj.jdbc.Driver

          url: jdbc:mysql://192.168.1.228:3306/cosunter_202112?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=GMT%2b8

          username: root

          password: IuZMLuPGqf0RR3Vh

        cosunter-slave-202112:

          type: com.alibaba.druid.pool.DruidDataSource

          driver-class-name: com.mysql.cj.jdbc.Driver

          url: jdbc:mysql://192.168.1.228:3306/cosunter_202112?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=GMT%2b8

          username: root

          password: IuZMLuPGqf0RR3Vh

      sharding:

        #默认数据源,未配置分片规则的表将走默认数据源

        default-datasource-name: ds0

        master-slave-rules:

          ds0:

            # 读写分离配置 随机

            load-balance-algorithm-type: round_robin

            # 主库数据源名称,负责数据写入

            master-data-source-name: master

            # 从库数据源名称列表,多个逗号分隔

            slave-data-source-names: slave

          ds202110:

            # 读写分离配置 随机

            load-balance-algorithm-type: round_robin

            # 主库数据源名称,负责数据写入

            master-data-source-name: cosunter-master-202110

            # 从库数据源名称列表,多个逗号分隔

            slave-data-source-names: cosunter-slave-202110

          ds202111:

            # 读写分离配置 随机

            load-balance-algorithm-type: round_robin

            # 主库数据源名称,负责数据写入

            master-data-source-name: cosunter-master-202111

            # 从库数据源名称列表,多个逗号分隔

            slave-data-source-names: cosunter-slave-202111

          ds202112:

            # 读写分离配置 随机

            load-balance-algorithm-type: round_robin

            # 主库数据源名称,负责数据写入

            master-data-source-name: cosunter-master-202112

            # 从库数据源名称列表,多个逗号分隔

            slave-data-source-names: cosunter-slave-202112

        #配置分表规则

        tables:

          #逻辑表名

          temp:

            #雪花算法自动生成,默认就是雪花算法

            key-generator:

              column: id

              type: SNOWFLAKE

            #数据节点 数据源{}.逻辑表名{}

            actual-data-nodes: ds$->{2021..2021}${(10..12).collect{t ->t.toString().padLeft(2,'0')}}.temp_$->{1..31}

            #拆分策略,什么样的数据放入那个库中

            database-strategy:

              #标准分片策略

              standard:

                #分片键

                shardingColumn: timepoint

                #精准分片算法

                preciseAlgorithmClassName: cn.baec.shardingjdbc.PreciseTempShardingDatabaseAlgorithm

                #范围分片算法

                rangeAlgorithmClassName: cn.baec.shardingjdbc.RangeTempShardingDatabaseAlgorithm

            #表分片策略

            table-strategy:

              standard:

                sharding-column: timepoint

                preciseAlgorithmClassName: cn.baec.shardingjdbc.PreciseTempShardingTableAlgorithm

                rangeAlgorithmClassName: cn.baec.shardingjdbc.RangeTempShardingTableAlgorithm

4.2.3开发分库分表算法

库精准分片算法:

public  class PreciseTempShardingDatabaseAlgorithm implements PreciseShardingAlgorithm<Date> 
{
    @Override
    public String doSharding(Collection<String> dsList, PreciseShardingValue<Date> preciseShardingValue) {
        Date value = preciseShardingValue.getValue();
        LocalDate localDate =
 value.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        int year =localDate.getYear();
        String month=
localDate.getMonthValue()<10?"0"+localDate.getMonthValue():String.valueOf(localDate.getMonthValue());
        String dsName=String.format("%s%d%s","ds",year,month);
        if (dsList.contains(dsName)){
            return dsName;
        }else {
            //todo 记录错误日志
            throw new IllegalArgumentException("库精准分片异常!精准分片值=" + value +
                    "; 可用的配置源=" + dsList.toString()+
                    "; 算出的数据源"+dsName);
        }
    }
}

表精准分片算法:

public class PreciseTempShardingTableAlgorithm implements
 PreciseShardingAlgorithm<Date> {
    @Override
public String doSharding(Collection<String> tableNameList,
 PreciseShardingValue<Date> preciseShardingValue) {
        Date value = preciseShardingValue.getValue();
        LocalDate localDate =
 value.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        String logicTable =preciseShardingValue.getLogicTableName();
        int day =localDate.getDayOfMonth();
        String tableName=String.format("%s_%d",logicTable,day);
        if (tableNameList.contains(tableName)){
            return tableName;
        }else{
            //todo 记录异常日志
            throw new IllegalArgumentException("表精准分片异常!精准分片值=" + value +
                    "; 可用的配置表=" + tableNameList.toString()+
                    "; 算出的真实表="+tableName);
        }
    }
}

库范围分片算法:

public class RangeTempShardingDatabaseAlgorithm implements RangeShardingAlgorithm<Date> {
    @Override
public Collection<String> doSharding(Collection<String> dsNameList,
 RangeShardingValue<Date> rangeShardingValue) {
        log.info("范围分库线程名字:"+Thread.currentThread().getName());
        Range<Date> valueRange = rangeShardingValue.getValueRange();
        Date lower = valueRange.lowerEndpoint();
        Date upper = valueRange.upperEndpoint();
        LocalDate lowerLocalDate =
                lower.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        LocalDate upperLocalDate =
                upper.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        int lowerYear = lowerLocalDate.getYear();
        int lowerMonth = lowerLocalDate.getMonthValue();
        int upperYear = upperLocalDate.getYear();
        int upperMonth = upperLocalDate.getMonthValue();
        String logicDsName = rangeShardingValue.getLogicTableName();
        List<String> actualDsNameList= new ArrayList<>();
        if (lowerYear == upperYear) {
            // 不跨年
            for(int month=lowerMonth;month<=upperMonth;month++){
                String actualDsName= ShardingJdbcUtils.getDsName(lowerYear,month);
                actualDsNameList.add(actualDsName);
            }
        } else {
            // 跨年 如2020-2022
            // 从最小年值开始计算
            for(int month=lowerMonth;month<=12;month++){
                String actualDsName= ShardingJdbcUtils.getDsName(lowerYear,month);
                actualDsNameList.add(actualDsName);
            }
            //取介于小年值与大年值
            for(int year=lowerYear+1;year<upperYear;year++){
                for(int month=1;month<=12;month++){
                    String actualDsName= ShardingJdbcUtils.getDsName(year,month);
                    actualDsNameList.add(actualDsName);
                }
            }
            //最大年值开始算
            for(int month=1;month<=upperMonth;month++){
                String actualDsName= ShardingJdbcUtils.getDsName(upperYear,month);
                actualDsNameList.add(actualDsName);
            }
        }
        if (!dsNameList.containsAll(actualDsNameList)) {
            //todo 记录错误日志
            throw new IllegalArgumentException("库范围分片异常!范围分片值=" +valueRange.toString()  +
                    "; 可用的配置源=" + dsNameList.toString()+
                    "; 算出的数据源="+actualDsNameList.toString());

        }
        //参考源码排序数据源,保存到threadLocal,在调用表范围分片算法时按顺序调用
        Collection<String> result = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
        result.addAll(actualDsNameList);
        ShardingJdbcUtils.getDsThreadLocal().set(new ArrayList<>(result));
        return actualDsNameList;
    }
}

表范围分片算法:

public class RangeTempShardingTableAlgorithm implements RangeShardingAlgorithm<Date> 
{
    @Override
public Collection<String> doSharding(Collection<String> tableNamesList,
 RangeShardingValue<Date> rangeShardingValue) {
        log.info("范围分表线程名字:"+Thread.currentThread().getName());
        List<String> rs=ShardingJdbcUtils.getDsThreadLocal().get();
        //取出数据源名称,如ds202110
        int dsNameYearMonth=0;
        if (ObjectUtil.isNotEmpty(rs)){
            dsNameYearMonth=ShardingJdbcUtils.getDsNameDateYearMonth(rs.get(0));
            rs.remove(0);
            ShardingJdbcUtils.getDsThreadLocal().set(rs);
        }
        Range<Date> valueRange = rangeShardingValue.getValueRange();
        Date lower = valueRange.lowerEndpoint();
        Date upper = valueRange.upperEndpoint();
        LocalDate lowerLocalDate =
                lower.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        LocalDate upperLocalDate =
                upper.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
        int lowerYear = lowerLocalDate.getYear();
        int lowerMonth = lowerLocalDate.getMonthValue();
        int lowerYearMonth = Integer.valueOf(lowerYear+""+lowerMonth);
        int upperYear = upperLocalDate.getYear();
        int upperMonth = upperLocalDate.getMonthValue();
        int upperYearMonth =Integer.valueOf(upperYear+""+upperMonth);
        String logicTableName = rangeShardingValue.getLogicTableName();
        Set<String> actualTalbeNameSet=new LinkedHashSet<>();
        //1、当前数据源中的数据,包含分片建查询上下限 分片键小值和大值年月=数据源年月
        if (dsNameYearMonth==lowerYearMonth&&dsNameYearMonth==upperYearMonth){
            //此种方式精确计算查询区间跨2月(28天或29天)的表名
            DateTime tempDate =DateUtil.date(lower);
            while(DateUtil.isIn(tempDate,lower,upper)){
                String actualTableName=String.format("%s_%d",logicTableName,tempDate.dayOfMonth());
                actualTalbeNameSet.add(actualTableName);
                //加1天,变成第二天的00:00:00
                tempDate=DateUtil.offsetDay(DateUtil.beginOfDay(tempDate),1);
            }
        }
        //2、当前数据源中的数据,只包含分片键下限,上限为当月最后一天(28或29或31号)    数据源年月=分片键小值年月且<分片键大值年月
        if (dsNameYearMonth==lowerYearMonth&&dsNameYearMonth<upperYearMonth){
            //此种方式精确计算查询区间跨2月(28天或29天)的表名
            DateTime tempDate =DateUtil.date(lower);
            while(DateUtil.isIn(tempDate,lower,DateUtil.endOfMonth(lower))){
                String actualTableName=String.format("%s_%d",logicTableName,tempDate.dayOfMonth());
                actualTalbeNameSet.add(actualTableName);
                //加1天,变成第二天的00:00:00
                tempDate=DateUtil.offsetDay(DateUtil.beginOfDay(tempDate),1);
            }
        }
        //3、当前数据源中的数据,只包含分片建上限,下限为当月1号   数据源年月=分片键大值年月且>分片键小值年月
        if (dsNameYearMonth==upperYearMonth&&dsNameYearMonth>lowerYearMonth){
            //此种方式精确计算查询区间跨2月(28天或29天)的表名
            DateTime tempDate =DateUtil.beginOfMonth(upper);
            while(DateUtil.isIn(tempDate,DateUtil.beginOfMonth(upper),upper)){
                String actualTableName=String.format("%s_%d",logicTableName,tempDate.dayOfMonth());
                actualTalbeNameSet.add(actualTableName);
                //加1天,变成第二天的00:00:00
                tempDate=DateUtil.offsetDay(tempDate,1);
            }
        }
        //4、当前数据源中的数据,在分片键上下限之间   数据源年月在分片键小值年月和大值年月中间
        if (dsNameYearMonth>lowerYearMonth&&dsNameYearMonth<upperYearMonth){
            //获取该数据源的当月首日
            DateTime tempDate = DateUtil.parse(dsNameYearMonth+""+"01", DatePattern.PURE_DATE_PATTERN);
            DateTime begin=tempDate;
            DateTime end=DateUtil.endOfMonth(tempDate);
            while(DateUtil.isIn(tempDate,begin,end)){
                String actualTableName=String.format("%s_%d",logicTableName,tempDate.dayOfMonth());
                actualTalbeNameSet.add(actualTableName);
                //加1天,变成第二天的00:00:00
                tempDate=DateUtil.offsetDay(tempDate,1);
            }

        }
        if (tableNamesList.containsAll(actualTalbeNameSet)) {
            //todo 记录错误日志
            throw new IllegalArgumentException("表范围分片异常!当前数据源名称:ds"+dsNameYearMonth+";范围分片值=" +valueRange.toString()
                    + "; 可用的配置表=" +tableNamesList.toString()
                    +"; 算出的真实表="+actualTalbeNameSet.toString());
        }
        return actualTalbeNameSet;
    }
}

分片算法工具类:

public class ShardingJdbcUtils {

    private static ThreadLocal<List<String>> dsThreadLocal = new ThreadLocal<>();
    private static final String dsPre="ds";

    public static ThreadLocal<List<String>> getDsThreadLocal() {
        return dsThreadLocal;
    }
    /**
     * 数字月份转字符串月份,小于10则左补0 如1->01
     * @param month 月份
     * @return
     */
    public static String monthToMonthStr(int month){
        return month<10?"0"+month:String.valueOf(month);
    }
    /**
     * 根据年、月组装数据源
     * @param year 年
     * @param month 月
     * @return 数据源名称
     */
    public static String getDsName(int year,int month){
        String monthStr=month<10?"0"+month:String.valueOf(month);
        return String.format("%s%d%s",dsPre,year,monthStr);
    }
    /**
     * 数据源名称转换年、月组合
     * @param dsName 数据源名称 如ds202110
     * @return 年月 如202110
     */
    public static int getDsNameDateYearMonth(String dsName) {
        if (dsName.startsWith(dsPre)){
            return Integer.valueOf(dsName.replace(dsPre,""));
        }else {
            return 0;
        }
    }
}

4.2.4配置文件修改

  1. 在配置文件中:启用sharding配置文件

  1. 去掉原有的数据源,否则会报错

5、实施验证

5.1验证未配置分片规则的表将走默认数据源

场景设置:

系统用户表sys_user,未进行分片,添加用户时则走默认主数据源,读取用户信息时则走默认从数据源

5.1.1添加用户:走默认主数据源

主数据库:

从数据库:(此处是为了模拟没有真正使用主从同步,真实情况应该从库也有的)

执行日志:

5.1.2获得用户信息:走默认从数据源

根据用户id,查询用户,获取数据走从库slave

主库数据:

从库数据:

执行结果:

执行日志:

5.2验证配置分片规则的表将走分库分表

场景设置:

温度数据表temp,进行分库分表,新增温度数据,根据分片字段,分片算法入库到对应的主库及表中,读取数据将按照分片规则,读取对应数据源的从库数据。

5.2.1添加温度数据

入库数据timepoint:2021-10-01,根据标准分片策略,将入库到ds202110.temp1

执行结果:入库到cosunter_202110.temp1

执行过程:库精确分片算法

表精确分片算法

执行日志:

关于自动生成id,由于配置了mybatis-plus默认会自动使用雪花算法生成id,一旦mybatis-plus自动生成id,则shardingjdbc就不会在生成,除非mybatis-plus的主键@TableId(type=IdType.AUTO),故保持框架不变默认雪花id即可

Mp主键设置uuid,shardingjdbc设置snowflake,则shardingjdbc主键自动生成失效

Mp主键设置auto  Shardingjdbc设置snowflake

Mp主键设置默认雪花,shardingjdbc设置snowflake

5.2.2读取温度数据操作符=

查询分片数据:根据时间=2021-10-01 10:00:00,走从数据库

执行日志:真正执行的sql就一条,走精准分片算法

如果没有实现范围分片算法,而使用了between and,则会报错:

5.2.3读取温度数据,操作符时between and

目前我们设置三个个库202110、202111、201112,每个库下面31张表,执行between and ,查询2021.10.31到2021.11.01的数据,则shardingjdbc应该从10月及11月这两个库中查询数据,如下图日志:

通过上图可知,跨库执行的sql执行过程,先根据分片键,算出所有的数据源,每个数据源在调用一次范围分片算法,算出该数据源对应的表,最后装配到一起去执行,即经范围分片算法算出几个数据源就执行几次范围分表算法

目前的设计策略是:

数据源按照月进行分库,如cosunter_202110,cosunter_202111

分表按照天进行分表,如temp_1...temp31

这样就会造成,在跨月查询时,执行了多余的sql,即数据源和表组装时采用了笛卡尔积的方式。如下图:查询10.30到11.01

如果在调用表范围分片算法时能够知道当前是那个数据源调用的,就可以根据库的命名规则知道是那一月的库,这样就可以精准返回对应的表!!但可惜shardingjdbc的dosharding()方法中没有提供该数据源名称。

源码追踪:

库范围分片算法,返回的数据源有序列表,但并不是按照这个顺序组装执行sql的

从源码中可以知道是通过自然顺序排列的

源码查看:几个数据源就调用几次表分片算法,然后封装成dataNode,即包含了要真正查询的数据源及数据表。

this.routeTables()

解决思路:在表分片算法dosharding()中获取当前的数据源,根据数据源名称(按月分库)可知道,当前数据源是那年哪月的数据,即可根据分片键,精准返回对应的数据表

通过ThreadLocal,把数据源列表保存到线程中去。

1、程序在调用库分片算法时,按照自然排序方式,把数据源名计算结果存储到ThreadLocal中

2、程序在调用表分片算法时,取出第一个运用到当前算法中,然后删除,并重新设置到ThreadLocal中,供下个数据源调用时使用

再次执行查询,结果如下所示:

5.2.4读取温度数据操其他操作符

如果操作符为>,>=,<,<=则会全库表路由(配置库及表的笛卡儿积),不会走库表分片算法,如下timepoint>=2021-10-23

支持的函数:count ,min,max,avg,groupby

查询10.31到11.1日的温度数据,按照user_id分组统计

返回结果

支持分页查询:

在项目中,可直接使用mybatis-plus分页查询,shardingjdbc会优化sql,归并查询结果。

场景设置:

  1. 数据库中准备10.30到11.01的数据
  2. 查询10.30到11.01日数据按照温度降序排列,分页设置current=1,size=2

11.01库中temp_1数据

10.31库中的数据

10.30日库中的数据

执行结果

执行日志

查询10.30到11.01日数据按照温度降序排列,分页设置current=2,size=2

猜你喜欢

转载自blog.csdn.net/kutianya518/article/details/120981280