1.请参见项目代码地址

一.基本增删改查框架搭建

请参看博客开源框架RuoYi-Vue学习之基础框架搭建(集成mybatis实现基本增删改查以及PageHelper分页)

二.AbstractRoutingDataSource

1.Spring boot提供了AbstractRoutingDataSource根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。实现可动态路由的数据源,在每次数据库查询操作前执行。它的抽象方法determineCurrentLookupKey()决定使用哪个数据源

2.org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource源码的介绍:
在这里插入图片描述
在这里插入图片描述

3.AbstractRoutingDataSource的多数据源动态切换的核心逻辑是:在程序运行时,把数据源数据源通过 AbstractRoutingDataSource 动态织入到程序中,灵活的进行数据源切换基于AbstractRoutingDataSource的多数据源动态切换,可以实现读写分离,这么做缺点也很明显,无法动态的增加数据源

实现逻辑:

  1. 定义DynamicDataSource类继承抽象类AbstractRoutingDataSource,并实现了determineCurrentLookupKey()方法。
  2. 把配置的多个数据源会放在AbstractRoutingDataSource的 targetDataSources和defaultTargetDataSource中,然后通过afterPropertiesSet()方法将数据源分别进行复制到resolvedDataSources和resolvedDefaultDataSource中。
  3. 调用AbstractRoutingDataSource的getConnection()的方法的时候,先调用determineTargetDataSource()方法返回DataSource在进行getConnection()。

在这里插入图片描述
在这里插入图片描述

三.实现动态数据源切换步骤

3.1 自定义多数据源切换注解@DataSource以及数据源名称枚举类DataSourceType
/**
 * 自定义多数据源切换注解
 * 优先级:先方法,后类,如果方法覆盖了类上的数据源类型,以方法的为准,否则以类上的为准
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TargetDataSource {
    /**
     * 切换数据源名称
     */
    DataSourceType value() default DataSourceType.MASTER;
}
/**
 * 数据源名称
 */
public enum DataSourceType {
    /**
     * 主库
     */
    MASTER,

    /**
     * 从库
     */
    SLAVE
}
3.2 编写数据源切换处理类DynamicDataSourceContextHolder
/**
 * 数据源切换处理
 */
@Slf4j
public class DynamicDataSourceContextHolder {
    /**
     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
     * 所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
     */
    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 设置数据源的变量
     */
    public static void setDataSourceType(String dataSourceType) {
        log.info("切换到{}数据源", dataSourceType);
        CONTEXT_HOLDER.set(dataSourceType);
    }

    /**
     * 获得数据源的变量
     */
    public static String getDataSourceType() {
        return CONTEXT_HOLDER.get();
    }

    /**
     * 清空数据源变量
     */
    public static void clearDataSourceType() {
        CONTEXT_HOLDER.remove();
    }
}
3.3 编写DynamicDataSource扩展AbstractRoutingDataSource抽象类,重写determineCurrentLookupKey方法
/**
 * 扩展Spring的AbstractRoutingDataSource抽象类,重写determineCurrentLookupKey方法。指定默认目标数据源以及指定目标数据源的映射map
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    /**
     * 指定默认目标数据源以及指定目标数据源的映射map(将lookup作为key)
     * @param defaultTargetDataSource
     * @param targetDataSources
     */
    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources) {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceContextHolder.getDataSourceType();
    }
}
3.3 编写druid配置多数据源
/**
 * druid 配置多数据源
 */
@Configuration
@Slf4j
public class DynamicDruidDataSourceConfig {
    @Bean
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave")
    public DataSource slaveDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource dataSource(DataSource masterDataSource, DataSource slaveDataSource) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);
        targetDataSources.put(DataSourceType.SLAVE.name(), slaveDataSource);
        return new DynamicDataSource(masterDataSource, targetDataSources);
    }
}
3.4 编写配置文件
##数据源配置
#1.使用最新的mysql连接驱动;2.访问数据库时无法识别时区(在中国可以选择Asia/Shanghai或者Asia/Hongkong,UTC会比中国时间早8个小时)
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#主库数据源
spring.datasource.druid.master.url=jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.druid.master.username=root
spring.datasource.druid.master.password=zaq12wsx
#从库数据源
spring.datasource.druid.slave.url=jdbc:mysql://localhost:3306/mashirro?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.druid.slave.username=root
spring.datasource.druid.slave.password=zaq12wsx
##项目相关配置

##Spring配置
#要激活某个profile,需要做的事就是将profile名称的列表赋值给spring.profiles.active属性。
spring.profiles.active=datasource

##开发环境配置
#服务器的HTTP端口
server.port=8088
#应用的访问路径
server.servlet.context-path=/

##日志配置
logging.level.com.mashirro=debug
logging.level.org.springframework=warn

##mybatis相关配置
#配置mapper映射xml路径(*通配符)
mybatis.mapper-locations=classpath:/mapper/*.xml
#加载全局的配置文件
mybatis.config-location=classpath:mybatis/mybatis-config.xml
#别名
#mybatis.type-aliases-package=com.mashirro.**.domain

##PageHelper分页插件配置(请参看官方文档https://pagehelper.github.io/docs/)
#分页插件会自动检测当前的数据库链接,自动选择合适的分页方式。 你可以配置helperDialect属性来指定分页插件使用哪种方言。
pagehelper.helper-dialect=mysql
#分页合理化参数,默认值为false。当该参数设置为true时,pageNum<=0 时会查询第一页,pageNum>pages(超过总数时),会查询最后一页。默认false 时,直接根据参数进行查询。
pagehelper.reasonable=true
#为了支持startPage(Object params)方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值,可以配置 pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值,默认值为pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero
pagehelper.params=count=countSql
#支持通过Mapper接口参数来传递分页参数,默认值false,分页插件会从查询方法的参数值中,自动根据上面params配置的字段中取值,查找到合适的值时就会自动分页。
pagehelper.support-methods-arguments=true
3.5 使用Spring Aop编写切面处理类–>多数据源处理
@Aspect
@Component
@Slf4j
public class DataSourceAspect {

    /**
     * 声明一个切入点
     *
     * @within:对象级别,用于拦截标注在类上面的注解
     * @annotation:方法级别,用于拦截标注在方法上面的注解
     */
    @Pointcut("@annotation(com.mashirro.dynamicdatasource.annotation.TargetDataSource)"
            + "|| @within(com.mashirro.dynamicdatasource.annotation.TargetDataSource)") //切入点表达式
    public void dsPointCut() {

    }

    /**
     * 环绕通知:
     * 通知方法的第一个参数必须是ProceedingJoinPoint类型。
     * 在通知正文中,对ProceedingJoinPoint调用proceed()将导致底层方法执行。around通知返回的值是方法调用方看到的返回值。
     */
    @Around("dsPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        TargetDataSource targetDataSource = getTargetDataSourceAnnotation(point);
        if (targetDataSource != null) {
            DynamicDataSourceContextHolder.setDataSourceType(targetDataSource.value().name());
        }
        try {
            return point.proceed();
        } finally {
            // 销毁数据源 在执行方法之后
            DynamicDataSourceContextHolder.clearDataSourceType();
            log.info("clear datasource");
        }
    }

    /**
     * 获取需要切换的数据源
     */
    public TargetDataSource getTargetDataSourceAnnotation(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        TargetDataSource targetDataSource = AnnotationUtils.findAnnotation(signature.getMethod(), TargetDataSource.class);
        if (Objects.nonNull(targetDataSource)) {
            return targetDataSource;
        }
        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), TargetDataSource.class);
    }
}
3.6 启动类上排除数据源自动配置
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
3.7 使用postman测试成功!

四.优化-连接池配置

在这里插入图片描述

4.1 编写druid连接池配置类
##连接池配置
# 初始连接数
spring.datasource.druid.initialSize=5
# 最小连接池数量
spring.datasource.druid.minIdle=10
# 最大连接池数量
spring.datasource.druid.maxActive=20
# 配置获取连接等待超时的时间
spring.datasource.druid.maxWait=60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
spring.datasource.druid.timeBetweenEvictionRunsMillis=60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
spring.datasource.druid.minEvictableIdleTimeMillis=300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
spring.datasource.druid.maxEvictableIdleTimeMillis=900000
# 配置检测连接是否有效,,要求是一个查询语句,常用select 'x'.如果validationQuery为null,testOnBorrow和testOnReturn和testWhileIdle都不会起作用。
spring.datasource.druid.validationQuery=SELECT 1 FROM DUAL
# 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
spring.datasource.druid.testWhileIdle=true
# 申请连接时执行validationQuery检测连接是否有效,true会降低性能。
spring.datasource.druid.testOnBorrow=false
# 归还连接时执行validationQuery检测连接是否有效,true会降低性能。
spring.datasource.druid.testOnReturn=false
/**
 * druid连接池配置属性
 *
 * @author ruoyi
 */
@Configuration
public class DruidProperties {
    @Value("${spring.datasource.druid.initialSize}")
    private int initialSize;
    @Value("${spring.datasource.druid.minIdle}")
    private int minIdle;
    @Value("${spring.datasource.druid.maxActive}")
    private int maxActive;
    @Value("${spring.datasource.druid.maxWait}")
    private int maxWait;
    @Value("${spring.datasource.druid.timeBetweenEvictionRunsMillis}")
    private int timeBetweenEvictionRunsMillis;
    @Value("${spring.datasource.druid.minEvictableIdleTimeMillis}")
    private int minEvictableIdleTimeMillis;
    @Value("${spring.datasource.druid.maxEvictableIdleTimeMillis}")
    private int maxEvictableIdleTimeMillis;
    @Value("${spring.datasource.druid.validationQuery}")
    private String validationQuery;
    @Value("${spring.datasource.druid.testWhileIdle}")
    private boolean testWhileIdle;
    @Value("${spring.datasource.druid.testOnBorrow}")
    private boolean testOnBorrow;
    @Value("${spring.datasource.druid.testOnReturn}")
    private boolean testOnReturn;

    public DruidDataSource dataSource(DruidDataSource datasource) {
        /** 配置初始化大小、最小、最大 */
        datasource.setInitialSize(initialSize);
        datasource.setMaxActive(maxActive);
        datasource.setMinIdle(minIdle);

        /** 配置获取连接等待超时的时间 */
        datasource.setMaxWait(maxWait);

        /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);

        /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);

        /**
         * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
         */
        datasource.setValidationQuery(validationQuery);
        /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
        datasource.setTestWhileIdle(testWhileIdle);
        /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
        datasource.setTestOnBorrow(testOnBorrow);
        /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
        datasource.setTestOnReturn(testOnReturn);
        return datasource;
    }
}
4.2 在配置多数据源bean时注入
    @Bean
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource(DruidProperties druidProperties) {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
//        return dataSource;
        return druidProperties.dataSource(dataSource);
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave")
    public DataSource slaveDataSource(DruidProperties druidProperties) {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
//        return dataSource;
        return druidProperties.dataSource(dataSource);
    }
4.3 测试生效!!

在这里插入图片描述

五.优化-配置druid监控(待补充…)

Logo

快速构建 Web 应用程序

更多推荐