分库分表ShardingJDBC最佳实践

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分库分表ShardingJDBC最佳实践相关的知识,希望对你有一定的参考价值。

1 添加依赖

<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-core</artifactId>
<version>$sharding.version</version>
</dependency>

2 分库分表数选择

根据未来两年的业务量,估算两年的业务总量M,单表数据量不能超过N(需要看具体业务场景,字段少的可以适量多一些,可与架构师及部门经验丰富的同事探讨,最大不要超过1000W);

总的分表数量K≥M/N,且K值向上取接近的最小2的次幂。

例如业务总量M=10亿,单表数量N=700W,则M/N≈143,向上取最小的2次幂为:分库分表ShardingJDBC最佳实践_分库分表,故总的分表数量为256。

可将分表数设定的尽可能的小,一台服务器存放多个库,业务增长后,磁盘不足时,可将该服务器上的整个库的数据迁移到新的服务器。

如总的分表数量为1024,可分为32个库:db0~db31,每个库分32张表:tb0~tb31,共16台服务器:server0~server15。

具体划分如下:

  • server0上有db0库和db16库
  • server1上有db1库和db17库
  • ……
  • server15上有db15库和db31库

如果业务发展一段时间后出现磁盘不足,可在申请16台服务器:server16~server31

  • server0上的db16库迁移至server16
  • server1上的db17库迁移至server17
  • ……
  • server15上的db31库迁移至server31

迁移后磁盘释放50%空间

3 分片键(拆分键)选择

分片键的选择一定要具有明显的业务特征,具体表现为:

  • 常用的已知的具体值,查询条件中要包含分片键,需要根据分片键确定在哪个库表,如果查询条件里没有分片键,将会遍历所有库表,这种情况是不允许的;
  • 分片键分辨度较高,比较分散,不能选择分辨度不高的的字段作为分片键,容易造成数据分布不均匀;
  • 分片键不能有偏移,比如分片键某个值不能远远大于其他值,这种情况也会造成数据分布不均匀;
  • 大小写,如果分片键在某些表里不区分大小写,那么分片键要统一大写或小写后用于分片,因此其他条件要考虑统一大小写后的情况;

目前项目里使用的分片键为商家编码,并统一为大写,因为:

  • 在查询分库分表数据时,商家编码是已知的,所以查询条件中包含商家编码;
  • 商家编码较多,能够保证按照商家编码分片后数据基本是均匀分布的;
  • 系统中不存在某个或某几个商家编码的数据量占据大部分的数据量;

4 分片算法

inline: Groovy的Inline表达式,可以支持SQL语句中的=和IN的操作,InlineShardingStrategy只支持单分片键;这种方式是最常用的,也是目前项目中在使用的。

其他分片策略参考:​​Sharding-JDBC分片策略​

4.1 分库算法

以32个库为例:Math.abs(库分片键.toUpperCase().hashCode()) % 32,将分片键商家编码统一大写后,取hashCode的绝对值,对32取模,32为分库数;

通用公式为:Math.abs(库分片键.toUpperCase().hashCode()) % 分库数

4.2 分表算法

以每个库32张表为例:(Math.abs(表分片键.toUpperCase().hashCode()) % 1024).intdiv(32),要保证分到某个库的数据,均匀分布到该库的每个表;

注意:此处不能直接用分库的公式,因为某个商家计算后分配到第i库,如果分表用同样的公式,则也只能落到第i表,导致一个库中只有一个表有数据。因此,需要先对一个较大的数取模,该较大的数为分库数分表数,记为m,本例中=3232=1024,将hash后的值分散到0-1023之间,在除以32,则最终结果被分配到0-31之间。

通用公式为:(Math.abs(表分片键.toUpperCase().hashCode()) % 分库数*分表数).intdiv(分库数)

5 全局(分布式)主键

分库分表,要求主键全局唯一,因为存在数据汇总的场景,如存放到ES、大数据等;因此不能使用数据库自增方式,需要一种全局唯一的主键生成方式;

5.1 shardingjdbc提供的主键

1. 添加maven依赖

<dependency>
<groupId>com.dangdang</groupId>
<artifactId>sharding-jdbc-self-id-generator</artifactId>
<version>1.4.2</version>
</dependency>

2. 在@Configuration类中创建Bean对象

@Bean
public IdGenerator getIdGenerator()
return new CommonSelfIdGenerator();

3. 使用

@Autowired
private IdGenerator idGenerator;
@Test
public void generateId()
long id = idGenerator.generateId().longValue();
......

5.2 雪花算法生成主键

很多插件如sharding-jdbc, mybatis-plus提供了雪花算法生成器,目前常用的是这种方式,生产的id时间上基本是有序的,使用很方便。

6 查询条件没有分片键如何处理

不允许没有分片键的查库操作;

查询或更新库操作条件里必须有分片键,否则就需要将分库分表数据存放到ES或建立查询条件与分片键的中间表;

ES或分片键中间表是为个别场景如页面多条件查询,用户获取不到分片键时采取的折中方案,绝大多数场景是能获取到分片键的,如果获取不到则可认为分片键的选取是不合理的,需要根据实际场景调整分片键。

ShardingJdbc-分表;分库分表;读写分离;一主多从+分表;一主多从+分库分表;公共表;数据脱敏;分布式事务

创建项目

一顿下一步,勾选web、lombok等插件

分表

导包

ShardingJdbc

官方最新稳定版4.1.1

<!-- https://mvnrepository.com/artifact/org.apache.shardingsphere/sharding-jdbc-spring-boot-starter -->
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>4.1.1</version>
</dependency>

Mysql

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.25</version>
</dependency>

Druid

<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.6</version>
</dependency>

MyBatisPlus

<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
  <version>3.4.3</version>
</dependency>

表结构

注意:之前我这表名叫table_0,结果一直报找不到表,后来发现,不能用关键字

CREATE TABLE `master_db`.`my_table_0` (
  `id` BIGINT (20) NOT NULL,
  `source` VARCHAR (20),
  `remark` VARCHAR (100),
  PRIMARY KEY (`id`)
);

CREATE TABLE `master_db`.`my_table_1` (
  `id` BIGINT (20) NOT NULL,
  `source` VARCHAR (20),
  `remark` VARCHAR (100),
  PRIMARY KEY (`id`)
);

Yml

application-sharding_table.yml

 
server:
  port: 80
spring:
  application:
    name: shardingJdbcDemo
  #shardingsphere相关配置
  shardingsphere:
    datasource:
      names: db   #库名,只是名字
      db: #数据源
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/master_db?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT
        username: root
        password: 123
    sharding: #指定表的分片规则
      tables: # 看到table "s" ,就知道可配置多个
        my_table: # 指定表名,此名必须和model中 @TableName(value = "my_table") 一致
          actualDataNodes: db.my_table_$->{0..1} #创建了两个表,下标0和1,所以此处配置你懂的
          tableStrategy:
            inline: # 指定表的分片策略
              shardingColumn: id #参与分片运算的列名
              algorithmExpression: my_table_$->{id % 2} #分片规则
          keyGenerator: # 指定表的主键生成策略
            type: SNOWFLAKE  #SNOWFLAKE
            column: id  #指定主键
    props:
      sql:
        show: true #输出日志

至此配置完毕,运行效果见下方代码

分库分表

java代码都不改,只改数据库及yml

数据库

master_db、 slave_db1、 slave_db2

库中分别创建my_table_0、my_table_1,结构同 上

Yml

application-sharding_master_slave.yml

spring:
  application:
    name: shardingJdbcDemo
  #shardingsphere相关配置
  shardingsphere:
    datasource:
      names: db0,db1,db2   #库名,只是名字
      db0: #主库
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/master_db?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT
        username: root
        password: 123
      db1: #从库1
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/slave_db1?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT
        username: root
        password: 123
      db2: #从库2
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/slave_db2?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT
        username: root
        password: 123
        #分库分表start
    sharding: #指定表的分片规则
      tables: # 看到table "s" ,就知道可配置多个
        my_table: # 指定表名,此名必须和model中 @TableName(value = "my_table") 一致
          actualDataNodes: db$->{0..2}.my_table_$->{0..1} #创建了两个表,下标0和1,所以此处配置你懂的
          databaseStrategy:
            inline: # 指定表的分片策略
              shardingColumn: id #参与分片运算的列名
              algorithmExpression: db$->{id % 3} #分片算法,分了三个库,所以要%3
          tableStrategy:
            inline: # 指定表的分片策略
              shardingColumn: id #参与分片运算的列名
              algorithmExpression: my_table_$->{id % 2} #分片算法
          keyGenerator: # 指定表的主键生成策略
            type: SNOWFLAKE  #SNOWFLAKE
            column: id  #指定主键
        #分库分表end
    props:
      sql:
        show: true #输出日志

然后继续访问第一步的save&select方法就可看到效果了,配置文件中写的主从库没有实际意义,起错名字了,实际都会往里面写

读写分离

数据库

模拟主从,不做MySql真正的主从同步

上面做表分片时已经创建好了主库master_db 再创建两个库 slave_db1 slave_db2

三个库表结构一致

image-20210602134547999

master_db表中无数据

slave_db1 表数据如下

​ "id" "source" "remark"
​ "1" "source1" "remark1"

slave_db2 表数据如下

​ "id" "source" "remark"
​ "1" "source2" "remark2"

Yml

application-master_slave.yml

spring:
  application:
    name: shardingJdbcDemo
  #shardingsphere相关配置
  shardingsphere:
    datasource:
      names: masterdb,slavedb1,slavedb2   #库名,只是名字
      masterdb: #主库
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/master_db?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT
        username: root
        password: 123
      slavedb1: #从库1
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/slave_db1?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT
        username: root
        password: 123
      slavedb2: #从库2
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/slave_db2?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT
        username: root
        password: 123

    masterslave:
      name: dbs
      master-data-source-name: masterdb #主库
      slave-data-source-names: slavedb1,slavedb2 #多个从库
      load-balance-algorithm-type: ROUND_ROBIN #RANDOM随机  ROUND_ROBIN 轮训
    props:
      sql:
        show: true #输出日志

这时,访问上面搭好的项目,即可进行读写分离,代码见上方链接

其他

只请求主库

在请求上方添加如下代码即可

    //    HintManager hintManager = HintManager.getInstance();
     //   hintManager.setMasterRouteOnly();
@GetMapping("select")
    public  PageInfo select(int pageNum,int pageSize,Long id){
    //    HintManager hintManager = HintManager.getInstance();
     //   hintManager.setMasterRouteOnly();
        PageHelper.startPage(pageNum,pageSize);
        QueryWrapper<Table0> queryWrapper=new QueryWrapper();
        if(!StringUtils.isEmpty(id)) {
            queryWrapper.eq("id", id);
        }
        List<Table0> list = table0Service.list(queryWrapper);
        PageInfo pageInfo = new PageInfo<>(list);
        return pageInfo;
    }

读写分离判断逻辑代码

大致的流程是shardingJdbc根据sql获得对应的sqlStatement,然后只有SelectStatement的才走从库

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.shardingsphere.masterslave.route.engine.impl;

import lombok.RequiredArgsConstructor;
import org.apache.shardingsphere.api.hint.HintManager;
import org.apache.shardingsphere.core.rule.MasterSlaveRule;
import org.apache.shardingsphere.sql.parser.sql.statement.SQLStatement;
import org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement;

import java.util.ArrayList;

/**
 * Data source router for master-slave.
 */
@RequiredArgsConstructor
public final class MasterSlaveDataSourceRouter {
    
    private final MasterSlaveRule masterSlaveRule;
    
    /**
     * Route.
     * 
     * @param sqlStatement SQL statement
     * @return data source name
     */
    public String route(final SQLStatement sqlStatement) {
        if (isMasterRoute(sqlStatement)) {
            MasterVisitedManager.setMasterVisited();
            return masterSlaveRule.getMasterDataSourceName();
        }
        return masterSlaveRule.getLoadBalanceAlgorithm().getDataSource(
                masterSlaveRule.getName(), masterSlaveRule.getMasterDataSourceName(), new ArrayList<>(masterSlaveRule.getSlaveDataSourceNames()));
    }
    
    private boolean isMasterRoute(final SQLStatement sqlStatement) {
        return containsLockSegment(sqlStatement) || !(sqlStatement instanceof SelectStatement) || MasterVisitedManager.isMasterVisited() || HintManager.isMasterRouteOnly();
    }
    
    private boolean containsLockSegment(final SQLStatement sqlStatement) {
        return sqlStatement instanceof SelectStatement && ((SelecStatement) sqlStatement).getLock().isPresent();
    }
}

一主多从+分表

上面实现了一主多从和表分片,现在把它整合到一起

java代码还是没改,主要改的还是Yml

Yml

application-sharding_db_table_oneMaster_slaves.yml

关键配置见 masterSlaveRule,分片配置未改

#一主多从+表分片
spring:
  application:
    name: shardingJdbcDemo
  #shardingsphere相关配置
  shardingsphere:
    datasource:
      names: db0,db1,db2   #库名,只是名字
      db0: #主库
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/master_db?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT
        username: root
        password: 123
      db1: #从库1
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/slave_db1?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT
        username: root
        password: 123
      db2: #从库2
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/slave_db2?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT
        username: root
        password: 123
    masterSlaveRule: # 这里配置这个规则的话,相当于是全局读写分离配置
      name: ds_rw # 名称,合法的字符串即可,但如果涉及到在读写分离的基础上设置分库分表,则名称需要有意义才可以,另外,虽然目前没有强制要求,但主从库配置需要配置在实际关联的主从库上,如果配置的数据源之间主从是断开的状态,那么可能会发生写入的数据对于只读会话无法读取到的问题
      # 如果一个会话发生了写入并且没有提交(显式打开事务),sharidng sphere在后续的路由中,select都会在主库执行,直到会话提交
      masterDataSourceName: db0 # 主库的DataSource名称
      slaveDataSourceNames: # 从库的DataSource列表,至少需要有一个
        - db1
        - db2
      loadBalanceAlgorithmType: ROUND_ROBIN #从库负载均衡算法类型,可选值:ROUND_ROBIN,RANDOM。若loadBalanceAlgorithmClassName存在则忽略该配置,默认为ROUND_ROBIN
    sharding: #指定表的分片规则
      tables: # 看到table "s" ,就知道可配置多个
        my_table: # 指定表名,此名必须和model中 @TableName(value = "my_table") 一致
          actualDataNodes: db$->{0..2}.my_table_$->{0..1} #创建了两个表,下标0和1,所以此处配置你懂的
          databaseStrategy:
            inline: # 指定表的分片策略
              shardingColumn: id #参与分片运算的列名
              algorithmExpression: db$->{id % 3} #分片算法,分了三个库,所以要%3
          tableStrategy:
            inline: # 指定表的分片策略
              shardingColumn: id #参与分片运算的列名
              algorithmExpression: my_table_$->{id % 2} #分片算法
          keyGenerator: # 指定表的主键生成策略
            type: SNOWFLAKE  #SNOWFLAKE
            column: id  #指定主键
    props:
      sql:
        show: true #输出日志

一主多从+分库分表

上面实现了一主多从和表分片,现在把它整合到一起

java代码还是没改,主要改的还是Yml

创建了两个主库两个从库master_db1、master_db2、slave_db1、slave_db2

下方配置为 master_db1slave_db1为主从关系、master_db2与slave_db2为主从关系

修改点有点多,查看标*****的位置

参考

Yml

application-sharding_db_table_masters_slaves.yml

#一主多从+表分片
spring:
  application:
    name: shardingJdbcDemo
  #shardingsphere相关配置
  shardingsphere:
    datasource:
      names: db0,db1,db2,db3   #库名,只是名字 ******************新增名称db3
      db0: #主库                       
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/master_db1?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT
        username: root
        password: 123
      db1: #主库1                         ****************** 添加主库
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/master_db2?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT
        username: root
        password: 123
      db2: #从库1
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/slave_db1?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT
        username: root
        password: 123
      db3: #从库2                          ******************添加从库
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/slave_db2?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT
        username: root
        password: 123
    sharding:
      master-slave-rules: # 新增配置 ******************
        db0: # 此处名字一定要配置和下面 master-data-source-name 名字一致的
          master-data-source-name: db0
          slave-data-source-names: db2
        db1: # 此处名字一定要配置和下面 master-data-source-name 名字一致的
          master-data-source-name: db1
          slave-data-source-names: db3
      tables: # 看到table "s" ,就知道可配置多个
        my_table: # 指定表名,此名必须和model中 @TableName(value = "my_table") 一致
          actualDataNodes: db$->{0..1}.my_table_$->{0..1} #******************每组有一主一从,所以是0..1
          databaseStrategy:
            inline: # 指定表的分片策略
              shardingColumn: id #参与分片运算的列名
              algorithmExpression: db$->{(id / 10).toBigInteger() % 2} # *******不能再用id%2方式了,会导致数据分配不均,所以需要自定义方法
          tableStrategy:
            inline: # 指定表的分片策略
              shardingColumn: id #参与分片运算的列名
              algorithmExpression: my_table_$->{id % 2} #分片算法
          keyGenerator: # 指定表的主键生成策略
            type: SNOWFLAKE  #SNOWFLAKE
            column: id  #指定主键
    props:
      sql:
        show: true #输出日志

公共表&数据脱敏

公共表

项目中会有很多字典表、配置表等等,此类表数据固定、量级不大,所以可以无需采用分库分表,每个库存一份;

当配置公共表后,每次对公共表进行操作,都会同步操作所有库中的公共表

数据库

分别在三个库中新增表pub_table

Java代码

生成pub_table相关的MybatisPlus代码

Yml

application-publicTable_encryptor.yml

spring:
  application:
    name: shardingJdbcDemo
  #shardingsphere相关配置
  shardingsphere:
    datasource:
      names: db0,db1,db2   #库名,只是名字
      db0: #主库
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/master_db?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT
        username: root
        password: 123
      db1: #从库1
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/slave_db1?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT
        username: root
        password: 123
      db2: #从库2
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/slave_db2?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT
        username: root
        password: 123
    sharding:
      default-data-source-name: db #如果有多个库,公共表只有一个库有,那么,就要指定有公共表的库,否则会报找不到表
      default-database-strategy:
        inline:
          sharding-column: id
          algorithm-expression: db$->{id % 3}
      broadcast-tables: pub_table #公共表配置,可以多个
    props:
      sql:
        show: true #输出日志

数据脱敏

参考

spring:
  application:
    name: shardingJdbcDemo
  #shardingsphere相关配置
  shardingsphere:
    datasource:
      names: db   #库名,只是名字
      db: #数据源
        type: com.alibaba.druid.pool.DruidDataSource
        driverClassName: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3306/master_db?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&serverTimezone=GMT
        username: root
        password: 123
    sharding: #指定表的分片规则
      # 数据脱敏规则配置---start
      encrypt-rule:
        encryptors:
          encryptor_MD5:
            type: MD5
            props:
              md5.key.value: 123456
          encryptor_aes:
            # 加密、解密器的名字,内置的为MD5,aes.
            # 可以自定义,实现
            # org.apache.shardingsphere.encrypt.strategy.spi.Encryptor
            # 或者
            # org.apache.shardingsphere.encrypt.strategy.spi.QueryAssistedEncryptor
            # 这两个接口即可
            type: aes
            props:
              aes.key.value: 123456
        tables:
          # tables
          my_table:
            columns:
              # 逻辑列,就是写SQL里面的列,因为实体类的名字和数据库的加密列一致,所以这里都是name
              source:
                # 原文列
                # plainColumn: password
                # 密文列,用来存储密文数据
                cipherColumn: source
                # 加密器名字
                encryptor: encryptor_MD5
                #  password1:
                # 原文列
                #     plainColumn: password1
                # 密文列,用来存储密文数据
                #   cipherColumn: aes_password
                # 加密器名字
              #   encryptor: encryptor_aes
      # 数据脱敏规则配置---end
      broadcast-tables: pub_table #公共表
      tables: # 看到table "s" ,就知道可配置多个
        my_table: # 指定表名,此名必须和model中 @TableName(value = "my_table") 一致
          actualDataNodes: db.my_table_$->{0..1} #创建了两个表,下标0和1,所以此处配置你懂的
          tableStrategy:
            inline: # 指定表的分片策略
              shardingColumn: id #参与分片运算的列名
              algorithmExpression: my_table_$->{id % 2} #分片算法
          keyGenerator: # 指定表的主键生成策略
            type: SNOWFLAKE  #SNOWFLAKE
            column: id  #指定主键
    props:
      sql:
        show: true #输出日志
      query.with.cipher.column: true #是否使用密文列查询

分布式事务

分库分表后,因为不是一个库了,所以一旦报错,数据无法回滚

Yml

使用上方 application-sharding_db_table_masters_slaves.yml

pom

        <!-- 使用 XA 事务时,需要引入此模块 -->
        <dependency>
            <groupId>io.shardingsphere</groupId>
            <artifactId>sharding-transaction-spring-boot-starter</artifactId>
            <version>3.1.0</version>
            <type>pom</type>
        </dependency>

Java

新增

package com.rollback.shardingjdbcdemo.demo.conf;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration
@EnableTransactionManagement
public class TransactionConfiguration {

    @Bean
    public PlatformTransactionManager txManager(final DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean
    public JdbcTemplate jdbcTemplate(final DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

修改save方法

saveByTransaction

package com.rollback.shardingjdbcdemo.demo.sharding.controller;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.rollback.shardingjdbcdemo.demo.sharding.model.Table0;
import com.rollback.shardingjdbcdemo.demo.sharding.service.Table0Service;
import org.apache.shardingsphere.api.hint.HintManager;
import org.apache.shardingsphere.transaction.annotation.ShardingTransactionType;
import org.apache.shardingsphere.transaction.core.TransactionType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Random;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author koala
 * @since 2021-05-31
 */
@RestController
@RequestMapping("/table0")
public class Table0Controller {
    @Autowired
    Table0Service table0Service;

    @GetMapping("save")
    public void save(int tag){
            table0Service.save(new Table0(){{setSource("source"+tag);setRemark("remark"+tag);}});
    }
    @Transactional
    @ShardingTransactionType(TransactionType.XA)  // 支持TransactionType.LOCAL, TransactionType.XA, TransactionType.BASE
    @GetMapping("saveByTransaction")
    public void saveByTransaction(int tag){
        for (int i = 0; i < 10; i++) {
            if(i==5){
                List list=null;
                list.add("error");
            }
            table0Service.save(new Table0(){{setSource("source"+tag);setRemark("remark"+tag);}});
        }
    }

    @GetMapping("select")
    public  PageInfo select(int pageNum,int pageSize,Long id){
//        HintManager hintManager = HintManager.getInstance();
//        hintManager.setMasterRouteOnly();
        PageHelper.startPage(pageNum,pageSize);
        QueryWrapper<Table0> queryWrapper=new QueryWrapper();
        if(!StringUtils.isEmpty(id)) {
            queryWrapper.eq("id", id);
        }
        queryWrapper.eq("source","source1");
        List<Table0> list = table0Service.list(queryWrapper);
        PageInfo pageInfo = new PageInfo<>(list);
        return pageInfo;
    }

    @GetMapping("select1")
    public  List<Table0> select1(int pageNum,int pageSize,Long id){
        QueryWrapper<Table0> queryWrapper=new QueryWrapper();
        if(!StringUtils.isEmpty(id)) {
            queryWrapper.eq("id", id);
        }
        //queryWrapper.eq("source","source1");
        List<Table0> list = table0Service.list(queryWrapper);
        return list;
    }
}

验证步骤:

  • 清空数据库

  • 执行saveByTransaction

  • 报错后观察数据库,一条都没有

  • 去掉

    ​ @Transactional
    ​ @ShardingTransactionType(TransactionType.XA) // 支持TransactionType.LOCAL, TransactionType.XA, TransactionType.BASE

    后,会出现保存部分数据的情况

参考官网

代码下载

代码见sharding包下,请求入口见 Table0Controller

以上是关于分库分表ShardingJDBC最佳实践的主要内容,如果未能解决你的问题,请参考以下文章

ShardingJdbc-分表;分库分表;读写分离;一主多从+分表;一主多从+分库分表;公共表;数据脱敏;分布式事务

ShardingJDBC 分库分表详解

分库分表最佳实践

ShardingJDBC第一篇:分库分表

mysql数据库分库分表shardingjdbc

ShardingJDBC分库分表配置