springboot自定义分库分表插件+代码

springboot分库分表

每天多学一点点~
博主最近遇到订单需求,在公司大牛指导下,自己写了个分库分表,在这里记录一下,如有不足之处,欢迎各位指出。
话不多说,这就开始吧…

1.springboot配置多个数据源

  1. springboot配置多个数据源,话不多说,直接上代码
/**
 * @Title: 多个数据源映射
 * @Date 2019/5/2 01:25
 * @Created by 爆裂无球
 */
@ConfigurationProperties(prefix = "spring.datasource")     //配置文件的信息,读取并自动封装成实体类
@Data
public class TulingDruidProperties {

    private String druid00username;

    private String druid00passwrod;

    private String druid00jdbcUrl;

    private String druid00driverClass;

    private String druid01username;

    private String druid01passwrod;

    private String druid01jdbcUrl;

    private String druid01driverClass;

    private String druid02username;

    private String druid02passwrod;

    private String druid02jdbcUrl;

    private String druid02driverClass;
}

AbstractRoutingDataSource 这个类,不知道的可以百度下,配置多数据源这个类很重要,可以灵活的切换,可以通过AOP或者手动编程设置当前的DataSource

/**
 * 多数据源类
 * Created by 爆裂无球 on 2019/4/16.          AbstractRoutingDataSource, 该类充当了DataSource的路由中介, 
 *                                            能有在运行时, 根据某种key值来动态切换到真正的DataSource上。
 */
@Slf4j
public class TulingMultiDataSource extends AbstractRoutingDataSource {
    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        return MultiDataSourceHolder.getDataSourceKey();
    }
}

根据当前线程来选择具体的数据源

/**
 * 多数据源key 缓存类  根据当前线程来选择具体的数据源
 * Created by 爆裂无球 on 2019/4/16.
 */
@Slf4j
public class MultiDataSourceHolder {

    /**
     *  设置动态选择的Datasource,这里的Set方法可以留给AOP调用,或者留给我们的具体的Dao层或者Service层中手动调用,在执行SQL语句之前。
     */

    private static final ThreadLocal<String> dataSourceHolder = new ThreadLocal<>();

    private static final ThreadLocal<String> tableIndexHolder = new ThreadLocal<>();

    /**
     * 保存数据源的key
     * @param dsKey
     */
    public static void setdataSourceKey(String dsKey) {
        dataSourceHolder.set(dsKey);
    }

    /**
     * 从threadLocal中取出key
     * @return
     */
    public static String getDataSourceKey() {
        return dataSourceHolder.get();
    }

    /**
     * 清除key
     */
    public static void clearDataSourceKey() {
        dataSourceHolder.remove();
    }

    public static String getTableIndex(){
        return tableIndexHolder.get();
    }

    public static void setTableIndex(String tableIndex){
         tableIndexHolder.set(tableIndex);
    }

    public static void clearTableIndex(){
        tableIndexHolder.remove();
    }
}

配置多个数据源,注入spring容器

/**
 * @Title: 配置多个数据源
 * @Date 2019/5/2 01:31
 * @Created by 爆裂无球
 */
@Slf4j
@Configuration
@EnableConfigurationProperties({TulingDsRoutingSetProperties.class, TulingDruidProperties.class})  //  @EnableConfigurationProperties注解的作用是:使使用 @ConfigurationProperties 注解的类生效。
@MapperScan(basePackages = "com.tuling.busi.dao")
public class DataSourceConfiguration {

    @Autowired
    private TulingDsRoutingSetProperties tulingDsRoutingSetProperties;

    @Autowired
    private TulingDruidProperties tulingDruidProperties;

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.druid00")
    public DataSource dataSource00() {

        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername(tulingDruidProperties.getDruid00username());
        dataSource.setPassword(tulingDruidProperties.getDruid00passwrod());
        dataSource.setUrl(tulingDruidProperties.getDruid00jdbcUrl());
        dataSource.setDriverClassName(tulingDruidProperties.getDruid00driverClass());
        return dataSource;
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.druid01")
    public DataSource dataSource01() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername(tulingDruidProperties.getDruid01username());
        dataSource.setPassword(tulingDruidProperties.getDruid01passwrod());
        dataSource.setUrl(tulingDruidProperties.getDruid01jdbcUrl());
        dataSource.setDriverClassName(tulingDruidProperties.getDruid01driverClass());
        return dataSource;
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.druid02")
    public DataSource dataSource02() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUsername(tulingDruidProperties.getDruid02username());
        dataSource.setPassword(tulingDruidProperties.getDruid02passwrod());
        dataSource.setUrl(tulingDruidProperties.getDruid02jdbcUrl());
        dataSource.setDriverClassName(tulingDruidProperties.getDruid02driverClass());
        return dataSource;
    }

    @Bean("tulingMultiDataSource")
    public TulingMultiDataSource dataSource() {
        // 自己的多数据源类 需要 继承 AbstractRoutingDataSource
        TulingMultiDataSource tulingMultiDataSource = new TulingMultiDataSource();

        Map<Object,Object> targetDataSources = new HashMap<>();
        targetDataSources.put("dataSource00",dataSource00());
        targetDataSources.put("dataSource01",dataSource01());
        targetDataSources.put("dataSource02",dataSource02());

        //把多个数据 和 多数据源  进行关联
        tulingMultiDataSource.setTargetDataSources(targetDataSources);
        //设置默认数据源
        tulingMultiDataSource.setDefaultTargetDataSource(dataSource00());

       //将索引字段和 数据源进行映射 ,方便 分库时候 根据取模的值 计算出是哪个库
        Map<Integer,String> setMappings = new HashMap<>();
        setMappings.put(0,"dataSource00");
        setMappings.put(1,"dataSource01");
        setMappings.put(2,"dataSource02");
        tulingDsRoutingSetProperties.setDataSourceKeysMapping(setMappings);

        return tulingMultiDataSource;

    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("tulingMultiDataSource") TulingMultiDataSource tulingMultiDataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        //设置数据源 为 上面的 自定义数据源
        sqlSessionFactoryBean.setDataSource(tulingMultiDataSource);
        //设置mybatis映射路径
        sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mybatis/mapper/*.xml"));
        return sqlSessionFactoryBean.getObject();
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) throws Exception {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    @Bean
    public DataSourceTransactionManager transactionManager(@Qualifier("tulingMultiDataSource") TulingMultiDataSource tulingMultiDataSource){
        return new DataSourceTransactionManager(tulingMultiDataSource);
    }

博主 在这里配置了 三个数据源 ,代码中都加了注释

  1. properties配置
#配置多个数据源属性(第一个数据库)
spring.datasource.druid00username=root
spring.datasource.druid00passwrod=root
spring.datasource.druid00jdbcUrl=jdbc:mysql://localhost:3306/tuling-multiDs00
spring.datasource.druid00driverClass=com.mysql.jdbc.Driver
#配置第二个数据源
spring.datasource.druid01username=root
spring.datasource.druid01passwrod=root
spring.datasource.druid01jdbcUrl=jdbc:mysql://localhost:3306/tuling-multiDs01
spring.datasource.druid01driverClass=com.mysql.jdbc.Driver

#配置第三个数据源
spring.datasource.druid02username=root
spring.datasource.druid02passwrod=root
spring.datasource.druid02jdbcUrl=jdbc:mysql://localhost:3306/tuling-multiDs02
spring.datasource.druid02driverClass=com.mysql.jdbc.Driver

mybatis.configuration.map-underscore-to-camel-case=true


logging.level.org.springframework = WARN

logging.level.com.tuling.busi.dao= DEBUG


#配置分表分库设置属性
#分三个数据库
tuling.dsroutingset.dataSourceNum=3
#每一个库分为5个相同的表结构
tuling.dsroutingset.tableNum=4
#指定路由的字段(必须指定)
tuling.dsroutingset.routingFiled=
tuling.dsroutingset.tableSuffixStyle=%04d
tuling.dsroutingset.tableSuffixConnect=_
# 配置路由策略  ROUTING_DS_TABLE_STATEGY 多库多表    ROUTGING_DS_STATEGY 多库一表  ROUTGIN_TABLE_STATEGY 一库夺标
tuling.dsroutingset.routingStategy=ROUTING_DS_TABLE_STATEGY

2.分库分表 策略模式

提交分库分表,无非三种

  1. ROUTING_DS_TABLE_STATEGY 多库多表
  2. ROUTGING_DS_STATEGY 多库一表
  3. ROUTGIN_TABLE_STATEGY 一库多标

既然如此,为了遵循开闭原则,很容易想到可以用设计模式中的 策略模式 进行分装,如果不知道啥事策略模式,请自行百度,这里不再赘述;

  1. 分库分表三种策略的实现
    在这里插入图片描述
    代码结构图
	/**
	 * @Title: 路由接口      策略模式接口
	 * @Date 2019/5/2 01:28
	 * @Created by 爆裂无球
	 */
	public interface ITulingRouting {
	
	    /**
	     * 根据规则计算出
	     * @param routingFieldValue  参数值  routingField      参数即路由字段
	     * @return
	     */
	    String calDataSourceKey(String routingFieldValue,String routingField) throws LoadRoutingStategyUnMatch,RoutingFiledArgsIsNull;
	
	
	    /**
	     * 计算routingFiled字段的 hashcode值
	     * @param routingFiled
	     * @return
	     */
	    Integer getRoutingFileHashCode(String routingFiled);
	
	    /**
	     * 计算一个库所在表的索引值
	     * @param routingFiled
	     * @return
	     */
	    String calTableKey(String routingFiled) throws LoadRoutingStategyUnMatch,RoutingFiledArgsIsNull;
	
	    String getFormatTableSuffix(Integer tableIndex) throws FormatTableSuffixException;
	}
	```
	
	```
	
	/**
	 * 路由规则抽象类     策略模式    并加入 检查配置路由参数和 策略是否相匹配
	 * Created by 爆裂无球 on 2019/4/16.
	 */
	@Slf4j
	@EnableConfigurationProperties(value = {TulingDsRoutingSetProperties.class})
	@Data
	public abstract class AbstractTulingRouting implements ITulingRouting ,InitializingBean{
	
	    @Autowired
	    private TulingDsRoutingSetProperties tulingDsRoutingSetProperties;
	
	    /**
	     * 获取路由key的hash值
	     * @param routingFiled 路由key
	     * @return
	     */
	    public Integer getRoutingFileHashCode(String routingFiled){
	
	        return Math.abs(routingFiled.hashCode());
	    }
	
	    /**
	     * 获取表的后缀
	     * @param tableIndex 表的索引值
	     * @return
	     */
	    public String getFormatTableSuffix(Integer tableIndex) {
	        StringBuffer stringBuffer = new StringBuffer(tulingDsRoutingSetProperties.getTableSuffixConnect());
	
	        try {
	            stringBuffer.append(String.format(getTulingDsRoutingSetProperties().getTableSuffixStyle(), tableIndex));
	        } catch (Exception e) {
	            log.error("格式化表后缀异常:{}",getTulingDsRoutingSetProperties().getTableSuffixStyle());
	            throw new FormatTableSuffixException(MultiDsErrorEnum.FORMAT_TABLE_SUFFIX_ERROR);
	        }
	        return stringBuffer.toString();
	    }
	
	
	    /**
	     * 工程在启动的时候 检查配置路由参数和 策略是否相匹配   因为继承了 InitializingBean
	     * @throws Exception
	     */
	    public void afterPropertiesSet() throws LoadRoutingStategyUnMatch{
	
	        switch (getTulingDsRoutingSetProperties().getRoutingStategy()) {
	            case TulingConstant.ROUTING_DS_TABLE_STATEGY:
	                checkRoutingDsTableStategyConfig();
	                break;
	            case TulingConstant.ROUTGING_DS_STATEGY:
	                checkRoutingDsStategyConfig();
	                break;
	            case TulingConstant.ROUTGIN_TABLE_STATEGY:
	                checkRoutingTableStategyConfig();
	                break;
	        }
	    }
	
	    /**
	     * 检查多库 多表配置
	     */
	    private void checkRoutingDsTableStategyConfig() {
	        if(tulingDsRoutingSetProperties.getTableNum()<=1 ||tulingDsRoutingSetProperties.getDataSourceNum()<=1){
	            log.error("你的配置项routingStategy:{}是多库多表配置,数据库个数>1," +
	                    "每一个库中表的个数必须>1,您的配置:数据库个数:{},表的个数:{}",tulingDsRoutingSetProperties.getRoutingStategy(),
	                    tulingDsRoutingSetProperties.getDataSourceNum(),tulingDsRoutingSetProperties.getTableNum());
	            throw new LoadRoutingStategyUnMatch(MultiDsErrorEnum.LOADING_STATEGY_UN_MATCH);
	        }
	    }
	
	    /**
	     * 检查多库一表的路由配置项
	     */
	    private void checkRoutingDsStategyConfig() {
	        if(tulingDsRoutingSetProperties.getTableNum()!=1 ||tulingDsRoutingSetProperties.getDataSourceNum()<=1){
	            log.error("你的配置项routingStategy:{}是多库一表配置,数据库个数>1," +
	                            "每一个库中表的个数必须=1,您的配置:数据库个数:{},表的个数:{}",tulingDsRoutingSetProperties.getRoutingStategy(),
	                    tulingDsRoutingSetProperties.getDataSourceNum(),tulingDsRoutingSetProperties.getTableNum());
	            throw new LoadRoutingStategyUnMatch(MultiDsErrorEnum.LOADING_STATEGY_UN_MATCH);
	        }
	    }
	
	    /**
	     * 检查一库多表的路由配置项
	     */
	    private void checkRoutingTableStategyConfig() {
	        if(tulingDsRoutingSetProperties.getTableNum()<=1 ||tulingDsRoutingSetProperties.getDataSourceNum()!=1){
	            log.error("你的配置项routingStategy:{}是一库多表配置,数据库个数=1," +
	                            "每一个库中表的个数必须>1,您的配置:数据库个数:{},表的个数:{}",tulingDsRoutingSetProperties.getRoutingStategy(),
	                    tulingDsRoutingSetProperties.getDataSourceNum(),tulingDsRoutingSetProperties.getTableNum());
	            throw new LoadRoutingStategyUnMatch(MultiDsErrorEnum.LOADING_STATEGY_UN_MATCH);
	        }
	    }

在这里插入图片描述

/**
 * @Title: 策略配置类与配置属性关联类
 * @Date 2019/5/1 11:22
 * @Created by 爆裂无球
 */
@Configuration
public class RoutingStategyConfig {

    /**
     *  多库多表
     * @return
     *  @ConditionalOnProperty  读取propeties文件中内容
     *
     *  属性name以及havingValue来实现的,其中name用来从application.properties中读取某个属性值。
        如果该值为空,则返回false;
        如果值不为空,则将该值与havingValue指定的值进行比较,如果一样则返回true;否则返回false。
        如果返回值为false,则该configuration不生效;为true则生效。
     *
     *
     *
     */
    @Bean
    @ConditionalOnProperty(prefix = "tuling.dsroutingset",name = "routingStategy",havingValue ="ROUTING_DS_TABLE_STATEGY")
    public ITulingRouting routingDsAndTbStrategy() {
        return new RoutingDsAndTbStrategy();
    }

    /**
     *  多库一表
     * @return
     */
    @Bean
    @ConditionalOnProperty(prefix = "tuling.dsroutingset",name = "routingStategy",havingValue ="ROUTGING_DS_STATEGY")
    public ITulingRouting routingDsStrategy() {
        return new RoutingDsStrategy();
    }

    /**
     *  一库夺多表
     * @return
     */
    @Bean
    @ConditionalOnProperty(prefix = "tuling.dsroutingset",name = "routingStategy",havingValue ="ROUTGIN_TABLE_STATEGY")
    public ITulingRouting routingTbStategy() {
        return new RoutingTbStategy();
    }
}

RoutingStategyConfig 这个类,博主通过@ConditionalOnProperty(prefix = “tuling.dsroutingset”,name = “routingStategy”,havingValue =“ROUTING_DS_TABLE_STATEGY”) 注解,直接读取properties里面的内容。你在properties里面配置什么策略,则在aop切面注入策略接口ITulingRouting时,就是这个类;

取模算法
a–hashcode值 正整数
b–数据库个数 正整数
a%b 的值一定小于b,所以一定在配置的数据源中;
博主这里定义了三个库,每个库四张表,所以一定会存入某个库中;
这只是简单的用了取模,各位可以根据实际情况选择不同的算法;

库的索引值计算:
假设路由字段是orderId,这里博主是取的是路由字段的hashcode,然后根据分库分表策略进行取模(如orderId是10086,其hashcode则是46730416,策略是多库多表,博主这里定义了三个库,每库四张表,则取模出的库的索引值则是 46730416%3=1,druid01这个库)

      //将索引字段和 数据源进行映射 ,方便 分库时候 根据取模的值 计算出是哪个库
        Map<Integer,String> setMappings = new HashMap<>();
        setMappings.put(0,"dataSource00");
        setMappings.put(1,"dataSource01");
        setMappings.put(2,"dataSource02");
        tulingDsRoutingSetProperties.setDataSourceKeysMapping(setMappings);

表的索引值计算:
同上;
在实体类中会有tableSuffix字段,在mybatis中
insert into order ${tableSuffix} …代表了表的索引值

  1. 自定义路由关键字注解,拦截数据源
/**
 * 路由注解
 * Created by 爆裂无球 on 2019/4/17.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Router {

    String routingFiled();

}
  1. Aop拦截自定义注解
/**
 * 拦截切面组件
 * Created by 爆裂无球 on 2019/4/17.
 */
@Component
@Aspect
@Slf4j
public class RoutingAspect {

    @Autowired
    private ITulingRouting routing;


    @Pointcut("@annotation(com.tuling.multidatasource.annotation.Router)")
    public void pointCut(){};

    @Before("pointCut()")
    public void before(JoinPoint joinPoint) throws LoadRoutingStategyUnMatch, RoutingFiledArgsIsNull, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        long beginTime = System.currentTimeMillis();
        //获取方法调用名称
        Method method = getInvokeMethod(joinPoint);

        //获取方法指定的注解
        Router router = method.getAnnotation(Router.class);
        //获取指定的路由key
        String routingFiled = router.routingFiled();

        //获取方法入参
        Object[] args = joinPoint.getArgs();


        boolean havingRoutingField = false;

        if(args!=null && args.length>0) {
            for(int index=0;index<args.length;index++) {
                //参数值
                String routingFieldValue = BeanUtils.getProperty(args[index],routingFiled);
                if(!StringUtils.isEmpty(routingFieldValue)) {
                    //根据路由关键字 计算出 哪个数据源
                    String dbKey = routing.calDataSourceKey(routingFieldValue,routingFiled);
                    //根据路由关键字 计算出 哪个表
                    String tableIndex = routing.calTableKey(routingFieldValue);
                    log.info("选择的Dbkey是:{},tableKey是:{}",dbKey,tableIndex);
                    havingRoutingField = true;
                    break;
                }else {

                }
            }

            //判断入参中没有路由字段
            if(!havingRoutingField) {
                log.warn("入参{}中没有包含路由字段:{}",args,routingFiled);
                throw new ParamsNotContainsRoutingField(MultiDsErrorEnum.PARAMS_NOT_CONTAINS_ROUTINGFIELD);
            }
        }

    }

    private Method getInvokeMethod(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature)signature;
        Method targetMethod = methodSignature.getMethod();
        return targetMethod;
    }

    /**
     * 清除线程缓存
     * @param joinPoint
     */
    @After("pointCut()")
    public void methodAfter(JoinPoint joinPoint){
        MultiDataSourceHolder.clearDataSourceKey();
        MultiDataSourceHolder.clearTableIndex();
    }

ITulingRouting,有兴趣的同学debug一下,博主在上文已经说明过了,通过RoutingStategyConfig进行配置的;
分库分表的计算通过切面进行。

3.项目启动流程

  1. 配置properties多数据源信息
    在这里插入图片描述
  2. 在controller或者service方法上面加上自定义注解 @Router() 并写入路由关键字
    在这里插入图片描述

4.运行结果

用posman进行测试

运行结果
![在这里插入图片描述](https://img-blog.csdnimg.cn/20190502193950920.pn
数据库
在这里插入图片描述

5.总结+源码

  1. 前置知识点
    1.1 spring aop 知识点
    1.2 自定义注解以及如何解析自定义注解
    1.3 springboot 整合druid mybatis
    1.4 熟悉多数据源 AbstractRoutingDataSource的工作原理
    1.5 ThreadLocal的应用
    1.6 设计模式 策略模式

  2. 插件名称解释 多库:在数据中有多个相同的数据库比如Order00 Order01 Order02 数据库 多表:在一个数据库中比如Order00中有四个order订单表 比如 order_0000 order_0001 order_0002 order_0003

  3. 分表分库策略 ①:多库多表策略(ROUTING_DS_TABLE_STATEGY) ②:一库多表策略(ROUTGING_DS_STATEGY) ③:多库一表策略(ROUTGIN_TABLE_STATEGY) 四:由于分库分表策略不同,导致数据库个数 和表的个数不同,可以出现错误配置,项目中作了启动配置策略检查 com.tuling.multidatasource.core.AbstractTulingRouting.afterPropertiesSet

  4. 由于分库分表策略不同,导致数据库个数 和表的个数不同,可以出现错误配置,项目中作了启动配置策略检查 com.tuling.multidatasource.core.AbstractTulingRouting.afterPropertiesSet

  5. 在自定义注解中 可以配置指定的路由key,然后在切面中去解析自定义注解获取到自定义的路由key

  6. 根据application.properties tuling.dsroutingset.routingStategy来指定条件装配策略

6.存在的问题

  1. 项目中crud都可以根据路由字段进行,但是路由字段是orderId,若想根据用户id查询到其所有的订单,那么就要根据userId再进行插入操作,会造成数据库信息冗余。
  2. 代码中若是不用注解则是默认数据,还不够灵活,适合作为插件使用。
  3. 不支持事务,也不能加入事务注解,否则数据可以插入,但是显示的插入信息不对

7.结语

世上无难事,只怕有心人,每天积累一点点,fighting!!!

  • 4
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 10
    评论
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值