shardingsphere 4.1.1多主多从集成dynamic多数据源与mybatis-plus


shardingsphere 4.1.1多主多从集成dynamic多数据源与mybatis-plus

最近接到一个需求,在一个老旧的项目中,需要集成读写分离框架,并且后续需要考虑扩展到分库存储,基于以上考虑,我将技术框架锁定在了shardingsphere

一、简介

Shardingsphere的源码地址:https://github.com/apache/shardingsphere/tree/4.1.1

Shardingsphere官网地址:https://shardingsphere.apache.org/

Apache ShardingSphere 定位为 Database Plus,旨在构建异构数据库之上的标准层和生态系统。它侧重于如何重用现有的数据库及其各自的上层,而不是创建一个新的数据库。目标是最小化或消除底层数据库碎片化带来的挑战

二、业务背景

我们的项目使用到了dynamic-datasource-spring-boot-starter实现动态数据源切换功能,我们的这个项目需要按分类分别调用不同的数据库,并且每个数据库还新增了一个从库,后续可能会增加从库来提升读的效率

我们的实际业务一次请求只需要切换一次数据源,所以重写了源框架获取数据源的操作

三、实现业务需求代码

1、创建一个springboot项目,并且配置对应的maven配置


        
            org.springframework.boot
            spring-boot-starter
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        


        
        
            org.projectlombok
            lombok
            true
        
        
        
            com.baomidou
            mybatis-plus-boot-starter
            3.4.3.1
        
        
            com.baomidou
            mybatis-plus-generator
            3.5.0
        
        
        
            org.apache.velocity
            velocity-engine-core
            2.3
        
        
            org.realityforge.org.jetbrains.annotations
            org.jetbrains.annotations
            1.7.0
        
        
        
            com.alibaba
            druid
            1.2.6
        
        
        
            org.apache.shardingsphere
            sharding-jdbc-spring-boot-starter
            4.1.1
        
        
        
            mysql
            mysql-connector-java
            8.0.28
        
        
        
            com.baomidou
            dynamic-datasource-spring-boot-starter
            3.1.1
        

2、配置数据源与sharding

spring:
  port: 9090     #端口号
  application:
    name: shardingdemo  #app名称
  datasource:
    dynamic:
      strict: true #设置严格模式,默认false不启动. 启动后在未匹配到指定数据源时候回抛出异常,不启动会使用默认数据源.
      datasource:
        master:
          type: com.alibaba.druid.pool.DruidDataSource        # 连接池
          driver-class-name: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://数据库IP:端口/数据库名(普通形式数据库)   #用于不做读写分离与分库分表的数据库           
          username: root                                      
          password: hiTKKihv5HU6v9                                    
  shardingsphere:
    datasource:
      names: miningpoolmaster,miningpoolslave,miningprofitmaster,miningprofitslave                                 # 数据源名字
      miningpoolmaster:
        type: com.alibaba.druid.pool.DruidDataSource        # 连接池
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://数据库IP:端口/数据库名(master1)               # 主库地址
        username: root                                      # 主库用户名
        password: hiTKKihv5HU6v9                                    # 主库密码
      miningpoolslave:
        type: com.alibaba.druid.pool.DruidDataSource        # 连接池
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://数据库IP:端口/数据库名(slave1)               # 从库地址
        username: root
        password: hiTKKihv5HU6v9
      miningprofitmaster:
        type: com.alibaba.druid.pool.DruidDataSource        # 连接池
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://数据库IP:端口/数据库名(master2)               
        username: root
        password: hiTKKihv5HU6v9
      miningprofitslave:
        type: com.alibaba.druid.pool.DruidDataSource        # 连接池
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://数据库IP:端口/数据库名(slave2)                # 从库地址
        username: root
        password: hiTKKihv5HU6v9
    sharding:
      #配置数据源的读写分离,但是数据库一定要做主从复制
      master-slave-rules:
        # 配置主从名称,可以任意取名字
        ms0:
          master-data-source-name: miningpoolmaster
          slave-data-source-names: miningpoolslave
          load-balance-algorithm-type: round_robin
        ms1:
          master-data-source-name: miningprofitmaster
          slave-data-source-names: miningprofitslave
          load-balance-algorithm-type: round_robin
      default-data-source-name: ms0
      tables:
        help_center_menu:
#          logic-lable: help_center_menu
          actual-data-nodes: ms0.help_center_menu
#          database-strategy:
#            hint:
#              algorithm-class-name: com.example.shardingdemo.config.HintShardingKeyAlgorithm
    props:
      sql:
        show: true

1、解析配置:

spring.datasource.dynamic.strict:若配置为true,需要注意下面的datasource里面的默认数据源必须为master,否则会抛异常

解决办法:

1、datasource:

? master:

2、dynamic.primary: 指定默认数据源

2、sharding配置

  • 首先将多个数据源配置好:miningpoolmaster,miningpoolslave,miningprofitmaster,miningprofitslave
    • 建议取别名,好像用_连接的时候会报错
  • 配置读写分离配置
    • 注意:将一组读写配置取一个组名,用于后续切换数据源操作
    • 设置默认数据源,可以设置为上面的组名
    • 配置需要路由的库,由于我的业务逻辑,需要按不同的表路由到指定的数据源中,并且每个数据源中的库表结构不一样,所以需要采用表路由方式将数据库进行与表绑定(适用与表不多的情况)

3、动态数据源配置

通过翻看Dynamic源码可知,它的主要配置类:DynamicDataSourceAutoConfiguration,所以我们可以进行如下的扩展:

package com.example.shardingdemo.config;


import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.provider.AbstractDataSourceProvider;
import com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties;
import com.example.shardingdemo.utils.SpringUtil;
import org.apache.shardingsphere.core.yaml.config.masterslave.YamlMasterSlaveRuleConfiguration;
import org.apache.shardingsphere.shardingjdbc.jdbc.core.datasource.MasterSlaveDataSource;
import org.apache.shardingsphere.shardingjdbc.jdbc.core.datasource.metadata.MasterSlaveDatabaseMetaData;
import org.apache.shardingsphere.shardingjdbc.spring.boot.sharding.SpringBootShardingRuleConfigurationProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.Map;

/**
 * 动态数据源配置:
 *
 * 使用{@link com.baomidou.dynamic.datasource.annotation.DS}注解,切换数据源
 *
 * @DS(DataSourceConfiguration.SHARDING_DATA_SOURCE_NAME)
 *
 * @author wanping
 * @date 2022/4/25 上午10:36
 */
@Configuration
@AutoConfigureBefore({DynamicDataSourceAutoConfiguration.class, SpringBootConfiguration.class})
public class DataSourceConfiguration {
    /**
     * 分表数据源名称
     */
    public static final String SHARDING_DATA_SOURCE_NAME = "sharding";
    /**
     * 动态数据源配置项
     */
    @Autowired
    private DynamicDataSourceProperties dynamicDataSourceProperties;
    /**
     * shardingjdbc有四种数据源,需要根据业务注入不同的数据源
     *
     * 

1. 未使用分片, 脱敏的名称(默认): shardingDataSource; *

2. 主从数据源: masterSlaveDataSource; *

3. 脱敏数据源:encryptDataSource; *

4. 影子数据源:shadowDataSource * * shardingjdbc默认就是shardingDataSource * 如果需要设置其他的可以使用 * @Resource(value="") 设置 */ @Lazy @Resource DataSource shardingDataSource; /** * 将shardingDataSource放到了多数据源(dataSourceMap)中 * 注意有个版本的bug,3.1.1版本 不会进入loadDataSources 方法,这样就一直造成数据源注册失败 */ @Bean public DynamicDataSourceProvider dynamicDataSourceProvider() { Map datasourceMap = dynamicDataSourceProperties.getDatasource(); return new AbstractDataSourceProvider() { @Override public Map loadDataSources() { Map dataSourceMap = createDataSourceMap(datasourceMap); // 将 shardingjdbc 管理的数据源也交给动态数据源管 dataSourceMap.put(SHARDING_DATA_SOURCE_NAME, shardingDataSource); return dataSourceMap; } }; } /** * 将动态数据源设置为首选的 * 当spring存在多个数据源时, 自动注入的是首选的对象 * 设置为主要的数据源之后,就可以支持shardingjdbc原生的配置方式了 * * @return */ @Primary @Bean public DataSource dataSource(DynamicDataSourceProvider dynamicDataSourceProvider) { DynamicRoutingDataSource dataSource = new DynamicRoutingDataSource(); dataSource.setPrimary(dynamicDataSourceProperties.getPrimary()); dataSource.setStrict(dynamicDataSourceProperties.getStrict()); dataSource.setStrategy(dynamicDataSourceProperties.getStrategy()); dataSource.setProvider(dynamicDataSourceProvider); dataSource.setP6spy(dynamicDataSourceProperties.getP6spy()); dataSource.setSeata(dynamicDataSourceProperties.getSeata()); return dataSource; } }

4、编写测试案例

1、dao层

通过生成工具生成dao与entity

HelpCenterMenu:
package com.example.shardingdemo.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.io.Serializable;
import java.util.Date;

/**
 * 

* 帮助中心菜单表 *

* * @author wanping * @since 2022-04-24 */ @Data @EqualsAndHashCode(callSuper = false) public class HelpCenterMenu implements Serializable { private static final long serialVersionUID = 1L; /** * 菜单主键 */ @TableId(value = "menu_id", type = IdType.AUTO) private Long menuId; /** * 菜单名称 */ private String menuNm; /** * 菜单图片链接 */ private String menuImgUrl; /** * 菜单父Id */ private Long pId; /** * 排序 */ private Integer sequence; /** * 是否禁用 */ @TableField("is_enable") private Boolean enable; /** * 语言类型(中文:Chinese,英文:English) */ private String languageType; /** * 创建时间 */ private Date createTime; /** * 修改时间 */ private Date updateTime; /** * 菜单级别 */ private Integer menuLevel; public static final String MENU_ID = "menu_id"; public static final String MENU_NM = "menu_nm"; public static final String MENU_IMG_URL = "menu_img_url"; public static final String P_ID = "p_id"; public static final String SEQUENCE = "sequence"; public static final String IS_ENABLE = "is_enable"; public static final String LANGUAGE_TYPE = "language_type"; public static final String CREATE_TIME = "create_time"; public static final String UPDATE_TIME = "update_time"; public static final String MENU_LEVEL = "menu_level"; }
HelpCenterMenuMapper:
package com.example.shardingdemo.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.shardingdemo.entity.HelpCenterMenu;
import org.apache.ibatis.annotations.Mapper;

/**
 * 

* 帮助中心菜单表 Mapper 接口 *

* * @author wanping * @since 2022-04-24 */ @Mapper public interface HelpCenterMenuMapper extends BaseMapper { }
HelpCenterMenuService:
package com.example.shardingdemo.service;

import com.baomidou.dynamic.datasource.annotation.DS;
import com.example.shardingdemo.dao.HelpCenterMenuMapper;
import com.example.shardingdemo.entity.HelpCenterMenu;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@DS("sharding")
public class HelpCenterMenuService {

    @Autowired
    private HelpCenterMenuMapper helpCenterMenuMapper;

    public List selectList(){
        return helpCenterMenuMapper.selectList(null);
    }
  
  	public int insert(HelpCenterMenu helpCenterMenu){
        return helpCenterMenuMapper.insert(helpCenterMenu);
    }
}

注意:由于咋们的动态数据源默认选的是master,所以这里需要指定sharding数据源

ShardingdemoApplicationTests:
  • 测试写入
@Test
    void insertMenus(){
        HelpCenterMenu helpCenterMenu = new HelpCenterMenu();
        helpCenterMenu.setMenuId(IdWorker.getId());
        helpCenterMenu.setMenuNm("测试00005");
        helpCenterMenu.setSequence(10);
        helpCenterMenu.setEnable(false);
        helpCenterMenu.setLanguageType("Chinese");
        helpCenterMenu.setCreateTime(new Date(System.currentTimeMillis()));
        helpCenterMenuService.insert(helpCenterMenu);
    }

写入主库结果如下图:

  • 测试读取

    @Test
        void contextLoads() {
            List helpCenterMenus = helpCenterMenuService.selectList();
            helpCenterMenus.forEach(f->{
                System.out.println(f.getMenuNm());
            });
        }
    

    读取从库如下图:

目录结构: