SpringBoot + MyBatisPlus + ShardingJDBC 分庫分表讀寫分離整合

本文描述在本地數據庫模擬分庫分表、讀寫分離的整合實現,假定會員數據按照 ID 取模進行分庫分表,分爲 2 個主庫,每一個庫分配一個讀庫,累計 100 張表。以下表所示:java

主/從
user_1 t_user_00 ~ t_user_49
user_slave_1 t_user_00 ~ t_user_49
user_2 t_user_50 ~ t_user_99
user_slave_2 t_user_50 ~ t_user_99

本文主要展現核心代碼,部分如 Controller、Service 層的測試代碼實現很是簡單,故而省略這部分代碼。mysql

依賴版本

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>2.1.3.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.1.3.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
    <version>2.1.3.RELEASE</version>
</dependency>
<dependency>
    <groupId>io.shardingsphere</groupId>
    <artifactId>sharding-jdbc</artifactId>
    <version>3.0.0.M1</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.12</version>
</dependency>

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

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.41</version>
</dependency>

數據準備

use user_1;
CREATE TABLE `t_user_00` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `t_user_01` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `t_user_02` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

use user_2;
CREATE TABLE `t_user_50` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `t_user_51` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `t_user_52` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

use user_slave_1;
CREATE TABLE `t_user_00` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `t_user_01` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `t_user_02` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

use user_slave_2;
CREATE TABLE `t_user_50` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `t_user_51` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `t_user_52` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

代碼實現

數據源配置

server:
  port: 23333
spring:
  application:
    name: pt-framework-demo
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource

datasource:
  default:
     driver-class-name: com.mysql.jdbc.Driver
     url: jdbc:mysql://127.0.0.1:3307/test?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;rewriteBatchedStatements=true&amp;autoReconnect=true&amp;failOverReadOnly=false
     username: root
     password: root
     test-on-borrow: false
     test-while-idle: true
     time-between-eviction-runs-millis: 18800
     filters: mergeStat,wall,slf4j
     connectionProperties: druid.stat.slowSqlMillis=2000
     validationQuery: SELECT 1
     poolPreparedStatements: true
  user:
    master:
      user1:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3307/user_1?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;rewriteBatchedStatements=true&amp;autoReconnect=true&amp;failOverReadOnly=false
        username: root
        password: root
        test-on-borrow: false
        test-while-idle: true
        time-between-eviction-runs-millis: 18800
        filters: mergeStat,wall,slf4j
        connectionProperties: druid.stat.slowSqlMillis=2000
        validationQuery: SELECT 1
        poolPreparedStatements: true
      user2:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3307/user_2?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;rewriteBatchedStatements=true&amp;autoReconnect=true&amp;failOverReadOnly=false
        username: root
        password: root
        test-on-borrow: false
        test-while-idle: true
        time-between-eviction-runs-millis: 18800
        filters: mergeStat,wall,slf4j
        connectionProperties: druid.stat.slowSqlMillis=2000
        validationQuery: SELECT 1
        poolPreparedStatements: true
    slave:
      user1:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3307/user_slave_1?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;rewriteBatchedStatements=true&amp;autoReconnect=true&amp;failOverReadOnly=false
        username: root
        password: root
        test-on-borrow: false
        test-while-idle: true
        time-between-eviction-runs-millis: 18800
        filters: mergeStat,wall,slf4j
        connectionProperties: druid.stat.slowSqlMillis=2000
        validationQuery: SELECT 1
        poolPreparedStatements: true
      user2:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://127.0.0.1:3307/user_slave_2?useUnicode=true&amp;characterEncoding=UTF-8&amp;zeroDateTimeBehavior=convertToNull&amp;rewriteBatchedStatements=true&amp;autoReconnect=true&amp;failOverReadOnly=false
        username: root
        password: root
        test-on-borrow: false
        test-while-idle: true
        time-between-eviction-runs-millis: 18800
        filters: mergeStat,wall,slf4j
        connectionProperties: druid.stat.slowSqlMillis=2000
        validationQuery: SELECT 1
        poolPreparedStatements: true

主從、讀寫分離

/**
 * Created by Captain on 01/03/2019.
 */
@Configuration
@MapperScan(basePackages = {"com.xxxx.framework.usermapper"}, sqlSessionFactoryRef = "userShardingSqlSessionFactory")
public class UserShardingDBConfiguration {

    @Value("${spring.datasource.type}")
    private Class<? extends DataSource> dataSourceType;

    private static final String USER_1_MASTER = "dsUser1Master";
    private static final String USER_1_SLAVE = "dsUser1Slave";
    private static final String USER_2_MASTER = "dsUser2Master";
    private static final String USER_2_SLAVE = "dsUser2Slave";
    private static final String USER_SHARDING_1 = "dsMasterSlave1";
    private static final String USER_SHARDING_2 = "dsMasterSlave2";

    private static final String USER_SHARDING_DATA_SOURCE = "userSharding";

    @Bean(USER_1_MASTER)
    @ConfigurationProperties(prefix = "datasource.user.master.user1")
    public DataSource dsUser1(){
        return DataSourceBuilder.create().type(dataSourceType).build();
    }

    @Bean(USER_2_MASTER)
    @ConfigurationProperties(prefix = "datasource.user.master.user2")
    public DataSource dsUser2(){
        return DataSourceBuilder.create().type(dataSourceType).build();
    }

    @Bean(USER_1_SLAVE)
    @ConfigurationProperties(prefix = "datasource.user.slave.user1")
    public DataSource dsUserSlave1(){
        return DataSourceBuilder.create().type(dataSourceType).build();
    }

    /**
     * user_2
     * @return
     */
    @Bean(USER_2_SLAVE)
    @ConfigurationProperties(prefix = "datasource.user.slave.user2")
    public DataSource dsUserSlave2(){
        return DataSourceBuilder.create().type(dataSourceType).build();
    }

    @Bean(USER_SHARDING_1)
    public DataSource masterSlave1(@Qualifier(USER_1_MASTER) DataSource dsUser1,@Qualifier(USER_1_SLAVE) DataSource dsUserSlave1) throws Exception {
        Map<String,DataSource> dataSourceMap = new HashMap<>();
        dataSourceMap.put(USER_1_MASTER, dsUser1);
        dataSourceMap.put(USER_1_SLAVE, dsUserSlave1);
        MasterSlaveRuleConfiguration ruleConfiguration = new MasterSlaveRuleConfiguration("dsUser1", USER_1_MASTER, Lists.newArrayList(USER_1_SLAVE));
        return MasterSlaveDataSourceFactory.createDataSource(dataSourceMap, ruleConfiguration, new ConcurrentHashMap<>());
    }

    @Bean(USER_SHARDING_2)
    public DataSource masterSlave2(@Qualifier(USER_2_MASTER) DataSource dsUser2,@Qualifier(USER_2_SLAVE) DataSource dsUserSlave2) throws Exception {
        Map<String,DataSource> dataSourceMap = new HashMap<>();
        dataSourceMap.put(USER_2_MASTER, dsUser2);
        dataSourceMap.put(USER_2_SLAVE, dsUserSlave2);
        MasterSlaveRuleConfiguration ruleConfiguration = new MasterSlaveRuleConfiguration("dsUser2", USER_2_MASTER, Lists.newArrayList(USER_2_SLAVE));
        return MasterSlaveDataSourceFactory.createDataSource(dataSourceMap, ruleConfiguration, new ConcurrentHashMap<>());
    }

    @Bean(USER_SHARDING_DATA_SOURCE)
    public DataSource dsUser(@Qualifier(USER_SHARDING_1) DataSource dsUser1, @Qualifier(USER_SHARDING_2) DataSource dsUser2) throws Exception {
        Map<String, DataSource> dataSourceMap = new HashMap<>();
        dataSourceMap.put("dsUser1", dsUser1);
        dataSourceMap.put("dsUser2", dsUser2);
        ShardingRuleConfiguration userRule = getUserRule();
        userRule.setDefaultDataSourceName("dsUser");
        return ShardingDataSourceFactory.createDataSource(dataSourceMap, userRule, new ConcurrentHashMap<>(), new Properties());
    }

    /**
     * 配置分片規則
     * @return
     */
    private ShardingRuleConfiguration getUserRule(){
        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
        shardingRuleConfig.setDefaultDatabaseShardingStrategyConfig(new StandardShardingStrategyConfiguration("id", new MemberIdShardingSchemeAlgorithm()));
        shardingRuleConfig.setDefaultTableShardingStrategyConfig(new StandardShardingStrategyConfiguration("id",new MemberIdShardingTableAlgorithm()));
        shardingRuleConfig.getBindingTableGroups().add("t_user");
        return shardingRuleConfig;
    }

    @Bean("userShardingSqlSessionFactory")
    public SqlSessionFactory userSqlSessionFactory(@Qualifier(USER_SHARDING_DATA_SOURCE) DataSource dataSource) throws Exception{
        MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:usermapper/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }

    @Bean("userTransaction")
    public DataSourceTransactionManager userTransactionManager(@Qualifier(USER_SHARDING_DATA_SOURCE) DataSource dataSource){
        return new DataSourceTransactionManager(dataSource);
    }

}

分庫策略

/**
 * CoreUser 分庫策略
 * Created by Captain on 01/03/2019.
 */
public class MemberIdShardingSchemeAlgorithm implements PreciseShardingAlgorithm<Integer>  {
    @Override
    public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Integer> shardingValue) {
        for ( String str : availableTargetNames ){
            int index = shardingValue.getValue() % 100;
            return str + (index > 49 ? "2" : "1");
        }
        return null;
    }

}

分表策略

/**
 * 會員信息分表策略,按照 id 分表
 * Created by Captain on 04/03/2019.
 */
public class MemberIdShardingTableAlgorithm implements PreciseShardingAlgorithm<Integer> {
    @Override
    public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Integer> shardingValue) {
        int index = shardingValue.getValue() % 100;
        return shardingValue.getLogicTableName() + "_" + (index < 10 ? "0" + index : index + "");
    }
}

實體類

/**
 * Created by Captain on 01/03/2019.
 */
@TableName("t_user")
public class User {

    @TableId(type = IdType.INPUT)
    private Integer id;
    private String name;
    private Integer age;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

Mapper

/**
 * Created by Captain on 04/03/2019.
 */
public interface UserMapper extends BaseMapper<User> {

}

測試預期

模擬過程沒有實際作主從同步,寫入「主庫」中的數據並不能自動同步至「從庫」,所以,插入數據後,須要手動寫入數據至對應的從庫,而且可對數據進行差別寫入,測試查詢時可根據差別來判斷讀寫分離是否生效。web

測試用例 預期結果
插入數據 id 指定爲 8902 user_1 中數據寫入成功
插入數據 id 指定爲 8952 user_2 中數據寫入成功
查詢 id 爲 8902 的數據 查詢到 user_slave_1 中的結果
查詢 id 爲 8952 的數據 查詢到 user_slave_2 中的結果
相關文章
相關標籤/搜索