基于springboot的mysql实现动态切换数据源

一、概述

在项目中的某些场景中,需要对数据库进行一些优化。常用的有如下的实现方法:读写分离、引入缓存技术、主从复制、分库分表等。今天我们来简单介绍一些如何在程序中实现动态切换数据源,可能某台服务器性能比较好,让流量多的方法执行切换到此数据源去操作等等。

当然这种思想也可以扩展实现为读写分离,主库(主数据源)只负责写操作,从库(从数据源)只负责读操作,前提是数据库之间需要提前搭建好主从复制环境。

接下来,我们就将总结一下如何在代码层面动态切换不同的数据源。

二、动态切换数据源具体实现

主要有以下几个步骤:

  • (1)、定义存放当前数据源的上下文环境;
  • (2)、定义数据源路由的配置;
  • (3)、定义数据源的配置;
  • (4)、自定义动态切换数据源的注解;
  • (5)、定义注解的AOP切面处理类;
  • (6)、定义业务类以及mapper接口、实现;
  • (7)、测试动态切换数据源;

首先需要事先创建三个数据库:

CREATE DATABASE /*!32312 IF NOT EXISTS*/`test01` /*!40100 DEFAULT CHARACTER SET utf8 */;

USE `test01`;

/*Table structure for table `user` */

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL COMMENT '主键ID',
  `name` varchar(255) DEFAULT NULL COMMENT '用户名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `user` */

insert  into `user`(`id`,`name`) values (1,'1号用户');

CREATE DATABASE /*!32312 IF NOT EXISTS*/`test02` /*!40100 DEFAULT CHARACTER SET utf8 */;

USE `test02`;

/*Table structure for table `user` */

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL COMMENT '主键ID',
  `name` varchar(255) DEFAULT NULL COMMENT '用户名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `user` */

insert  into `user`(`id`,`name`) values (2,'2号用户');

CREATE DATABASE /*!32312 IF NOT EXISTS*/`test03` /*!40100 DEFAULT CHARACTER SET utf8 */;

USE `test03`;

/*Table structure for table `user` */

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL COMMENT '主键ID',
  `name` varchar(255) DEFAULT NULL COMMENT '用户名称',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

/*Data for the table `user` */

insert  into `user`(`id`,`name`) values (3,'3号用户');

注意:这里为了方便,直接使用的是同一台数据库服务器里面的三个不同的数据库信息,讲道理应该使用三台不同的数据库服务器真实一些,小伙伴们可以在不同的数据库服务器上进行测试,结果应该也是一样的。

然后,创建一个springboot项目,项目结构如下图:

application.yml配置如下:

spring:
  datasource:
    mysql:
      type-aliases-package: com.wsh.springboot.dynamicdatasource.mapper
      mapper-location: classpath:/mapper/*.xml
      config-location: classpath:/mybatis-config.xml
      ##此处为了方便,仅仅使用本地的三个数据库来区分动态切换数据源信息,小伙伴们可以扩展,使用不同服务器的不同数据库连接来测试一下,应该也是没有问题的.
      datasource01:  #数据源1
        url: jdbc:mysql://localhost:3306/test01?useUnicode=true&characterEncoding=utf-8&useSSL=true
        username: root
        password: wsh0905
        driver-class-name: com.mysql.cj.jdbc.Driver
      datasource02:  #数据源2
        url: jdbc:mysql://localhost:3306/test02?useUnicode=true&characterEncoding=utf-8&useSSL=true
        username: root
        password: wsh0905
        driver-class-name: com.mysql.cj.jdbc.Driver
      datasource03:  #数据源3
        url: jdbc:mysql://localhost:3306/test03?useUnicode=true&characterEncoding=utf-8&useSSL=true
        username: root
        password: wsh0905
        driver-class-name: com.mysql.cj.jdbc.Driver

mybatis-config.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <!-- 使全局的映射器启用或禁用缓存。 -->
        <setting name="cacheEnabled" value="true" />
        <!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。 -->
        <setting name="lazyLoadingEnabled" value="true" />
        <!-- 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。 -->
        <setting name="aggressiveLazyLoading" value="true"/>
        <!-- 是否允许单条sql 返回多个数据集  (取决于驱动的兼容性) default:true -->
        <setting name="multipleResultSetsEnabled" value="true" />
        <!-- 是否可以使用列的别名 (取决于驱动的兼容性) default:true -->
        <setting name="useColumnLabel" value="true" />
        <!-- 允许JDBC 生成主键。需要驱动器支持。如果设为了true,这个设置将强制使用被生成的主键,有一些驱动器不兼容不过仍然可以执行。  default:false  -->
        <setting name="useGeneratedKeys" value="false" />
        <!-- 指定 MyBatis 如何自动映射 数据基表的列 NONE:不隐射 PARTIAL:部分  FULL:全部  -->
        <setting name="autoMappingBehavior" value="PARTIAL" />
        <!-- 这是默认的执行类型  (SIMPLE: 简单; REUSE: 执行器可能重复使用prepared statements语句;BATCH: 执行器可以重复执行语句和批量更新)  -->
        <setting name="defaultExecutorType" value="SIMPLE" />

        <setting name="defaultStatementTimeout" value="25" />

        <setting name="defaultFetchSize" value="100" />

        <setting name="safeRowBoundsEnabled" value="false" />
        <!-- 使用驼峰命名法转换字段。 -->
        <setting name="mapUnderscoreToCamelCase" value="true" />
        <!-- 设置本地缓存范围 session:就会有数据的共享  statement:语句范围 (这样就不会有数据的共享 ) default:session -->
        <setting name="localCacheScope" value="SESSION" />
        <!-- 默认为OTHER,为了解决oracle插入null报错的问题要设置为NULL -->
        <setting name="jdbcTypeForNull" value="NULL" />
        <setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString" />
    </settings>

</configuration>

【a】定义存放当前数据源的上下文环境

数据源上下文环境,便于程序中可以随时取到当前的数据源,它主要利用ThreadLocal封装,因为ThreadLocal是线程隔离的,具有线程安全的优势。

具体代码如下:

package com.wsh.springboot.dynamicdatasource.context;

/**
 * @Description: 利用ThreadLocal封装的保存数据源上线的上下文
 * @author: weishihuai
 */
public class DataSourceContext {

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

    /**
     * 设置当前数据源
     */
    public static void set(String datasourceType) {
        CONTEXT.set(datasourceType);
    }

    /**
     * 获取当前数据源
     */
    public static String get() {
        return CONTEXT.get();
    }

    /**
     * 清除当前数据源
     */
    public static void clear() {
        CONTEXT.remove();
    }

}

【b】定义数据源路由的配置

路由在动态切换数据源的核心部分。Spring提供了AbstractRoutingDataSource类,可以根据用户定义的规则选择当前的数据源,作用就是在执行查询之前,设置使用的数据源,实现动态路由的数据源,在每次数据库查询操作前执行它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。

具体代码如下:

package com.wsh.springboot.dynamicdatasource.config;

import com.wsh.springboot.dynamicdatasource.context.DataSourceContext;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * @Description: 数据源路由的配置, 继承AbstractRoutingDataSource类,重写determineCurrentLookupKey()方法
 * @author: weishihuai
 */
public class DataSourceRouterConfig extends AbstractRoutingDataSource {

    /**
     * 最终的determineCurrentLookupKey返回的是从DataSourceContext上下文中获取的,因此在动态切换数据源的时候(即AOP切面处理时)应该给DataSourceContext赋值
     */
    @Override
    protected Object determineCurrentLookupKey() {
        //从上下文环境中获取当前数据源信息
        return DataSourceContext.get();
    }

}

【c】定义数据源的配置

主要包括:主数据源配置、sqlSessionFactory等的配置。

主要代码如下:

定义mybatis相关配置属性类:

package com.wsh.springboot.dynamicdatasource.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
 * @Description: mybatis相关配置属性类
 * @author: weishihuai
 */
@ConfigurationProperties("spring.datasource.mysql")
@Component
public class MapperConfigProperties {

    private String typeAliasesPackage;

    private String mapperLocation;

    private String configLocation;

    public String getTypeAliasesPackage() {
        return typeAliasesPackage;
    }

    public void setTypeAliasesPackage(String typeAliasesPackage) {
        this.typeAliasesPackage = typeAliasesPackage;
    }

    public String getMapperLocation() {
        return mapperLocation;
    }

    public void setMapperLocation(String mapperLocation) {
        this.mapperLocation = mapperLocation;
    }

    public String getConfigLocation() {
        return configLocation;
    }

    public void setConfigLocation(String configLocation) {
        this.configLocation = configLocation;
    }
}

数据源配置类:

package com.wsh.springboot.dynamicdatasource.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.wsh.springboot.dynamicdatasource.constants.Constants;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @Description: 数据源配置类
 * @author: weishihuai
 */
@Configuration
@MapperScan(basePackages = "com.wsh.springboot.dynamicdatasource.mapper", sqlSessionFactoryRef = "sqlSessionFactory")
public class DataSourceConfig {

    /**
     * 数据源1
     *
     * @Primary: 标志这个 Bean 如果在多个同类 Bean 候选时,该 Bean 优先被考虑。
     * 多数据源配置的时候注意,必须要有一个主数据源,用 @Primary 标志该 Bean
     */
    @Primary
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.mysql.datasource01")
    public DataSource datasource01() {
        return new DruidDataSource();
    }

    /**
     * 数据源2
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.mysql.datasource02")
    public DataSource datasource02() {
        return new DruidDataSource();
    }

    /**
     * 数据源3
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.mysql.datasource03")
    public DataSource datasource03() {
        return new DruidDataSource();
    }

    /**
     * 多数据源需要自己设置sqlSessionFactory
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(MapperConfigProperties mapperConfigProperties) throws Exception {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(routingDataSource());
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        // 实体类对应的位置
        bean.setTypeAliasesPackage(mapperConfigProperties.getTypeAliasesPackage());
        // mybatis的XML的配置
        bean.setMapperLocations(resolver.getResources(mapperConfigProperties.getMapperLocation()));
        bean.setConfigLocation(resolver.getResource(mapperConfigProperties.getConfigLocation()));
        return bean.getObject();
    }

    /**
     * 设置事务,事务需要知道当前使用的是哪个数据源才能进行事务处理
     */
    @Bean
    public DataSourceTransactionManager dataSourceTransactionManager() {
        return new DataSourceTransactionManager(routingDataSource());
    }

    /**
     * 设置数据源路由,通过DataSourceRouterConfig类中的determineCurrentLookupKey()方法返回值决定使用哪个数据源
     */
    @Bean
    public AbstractRoutingDataSource routingDataSource() {
        DataSourceRouterConfig dynamicDataSource = new DataSourceRouterConfig();
        Map<Object, Object> targetDataSources = new HashMap<>(16);
        targetDataSources.put(Constants.DATA_SOURCE_DATASOURCE01, datasource01());
        targetDataSources.put(Constants.DATA_SOURCE_DATASOURCE02, datasource02());
        targetDataSources.put(Constants.DATA_SOURCE_DATASOURCE03, datasource03());
        dynamicDataSource.setTargetDataSources(targetDataSources);
        dynamicDataSource.setDefaultTargetDataSource(datasource01());
        return dynamicDataSource;
    }

}

【d】自定义动态切换数据源的注解

定义一个@DynamicSwitchDataSource注解,拥有两个属性:

  • 当前的数据源;
  • 是否清除当前的数据源;

@DynamicSwitchDataSource作用与方法上,该注解的主要作用就是进行数据源的切换,在dao层进行操作数据库的时候,可以在方法上注明表示的是当前使用哪个数据源。

具体代码如下:

package com.wsh.springboot.dynamicdatasource.constants;

/**
 * @Description: 数据源名称常量类
 * @author: weishihuai
 */
public class Constants {
    public static final String DATA_SOURCE_DATASOURCE01 = "datasource01";
    public static final String DATA_SOURCE_DATASOURCE02 = "datasource02";
    public static final String DATA_SOURCE_DATASOURCE03 = "datasource03";
}
package com.wsh.springboot.dynamicdatasource.annotation;

import com.wsh.springboot.dynamicdatasource.constants.Constants;

import java.lang.annotation.*;

/**
 * @Description: 自定义注解,主要用于标注在类上面,实现动态切换数据源
 * @author: weishihuai
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DynamicSwitchDataSource {
    /**
     * 默认数据源为datasource01
     */
    String value() default Constants.DATA_SOURCE_DATASOURCE01;

    /**
     * 清除
     */
    boolean clear() default true;

}

【e】定义注解的AOP切面处理类

为了实现DynamicSwitchDataSource注解能够实现动态切换数据源的功能,我们需要使用AOP切面功能,使用@Around环绕通知,找到所有方法上带有@DynamicSwitchDataSource注解的方法,然后取出注解上配置的数据源,设置到我们前面定义的全局保存当前数据源上下文的DataSourceContext,就实现了将当前方法上配置的数据源注入到全局作用域当中。

为了保证切面的优先级高于事务切面优先级,我们需要在启动类加上注解:@EnableTransactionManagement(order = 10),如下所示:

@SpringBootApplication
@EnableTransactionManagement(order = 10)
public class ServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServiceApplication.class, args);
    }

}

AOP具体代码如下:

package com.wsh.springboot.dynamicdatasource.aop;

import com.wsh.springboot.dynamicdatasource.annotation.DynamicSwitchDataSource;
import com.wsh.springboot.dynamicdatasource.context.DataSourceContext;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @Description: 数据源切换AOP切面
 * @author: weishihuai
 * 说明:指定@Order(value = 1)保证切面优先级高于事务切面优先级[@EnableTransactionManagement(order = 10)].
 */
@Aspect
@Order(value = 1)
@Component
public class DataSourceContextAop {


    @Around("@annotation(com.wsh.springboot.dynamicdatasource.annotation.DynamicSwitchDataSource)")
    public Object dynamicSwitchDataSource(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        boolean clear = false;
        try {
            MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
            Method method = signature.getMethod();
            DynamicSwitchDataSource dynamicSwitchDataSource = method.getAnnotation(DynamicSwitchDataSource.class);
            clear = dynamicSwitchDataSource.clear();
            //给DataSourceContext上下文赋值为当前数据源
            DataSourceContext.set(dynamicSwitchDataSource.value());
            System.out.println("当前切换数据源至:" + dynamicSwitchDataSource.value());
            return proceedingJoinPoint.proceed();
        } finally {
            if (clear) {
                DataSourceContext.clear();
            }
        }
    }

}

【f】定义业务类以及mapper接口、实现

package com.wsh.springboot.dynamicdatasource.mapper;

import org.apache.ibatis.annotations.Mapper;

import java.util.Map;

@Mapper
public interface UserMapper {

    Map<String,Object> getUserInfo();

}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.wsh.springboot.dynamicdatasource.mapper.UserMapper">

    <select id="getUserInfo" resultType="java.util.Map">
        SELECT * FROM USER
    </select>

</mapper>
package com.wsh.springboot.dynamicdatasource.service;

import com.wsh.springboot.dynamicdatasource.annotation.DynamicSwitchDataSource;
import com.wsh.springboot.dynamicdatasource.constants.Constants;
import com.wsh.springboot.dynamicdatasource.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Map;

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @DynamicSwitchDataSource(Constants.DATA_SOURCE_DATASOURCE01)
    public void getUserInfoFromDBS01() {
        Map<String, Object> userInfo = userMapper.getUserInfo();
        System.out.println(userInfo);
    }

    @DynamicSwitchDataSource(Constants.DATA_SOURCE_DATASOURCE02)
    public void getUserInfoFromDBS02() {
        Map<String, Object> userInfo = userMapper.getUserInfo();
        System.out.println(userInfo);
    }

    @DynamicSwitchDataSource(Constants.DATA_SOURCE_DATASOURCE03)
    public void getUserInfoFromDBS03() {
        Map<String, Object> userInfo = userMapper.getUserInfo();
        System.out.println(userInfo);
    }

}

【g】测试动态切换数据源

这里我们使用单元测试进行测试:

@SpringBootTest
class ServiceApplicationTests {

    @Autowired
    private UserService userService;

    @Test
    void datasource01() {
        userService.getUserInfoFromDBS01();
    }

    @Test
    void datasource02() {
        userService.getUserInfoFromDBS02();
    }

    @Test
    void datasource03() {
        userService.getUserInfoFromDBS03();
    }

}

分别启动datasource01()、datasource02()、datasource03()进行测试:

观察后端打印日志信息:

datasource01():

当前切换数据源至:datasource01
{name=1号用户, id=1}

datasource02():

当前切换数据源至:datasource02
{name=2号用户, id=2}

datasource03():

当前切换数据源至:datasource03
{name=3号用户, id=3}

可见,我们成功实现了动态切换数据源功能,在实际项目中,可以借鉴此思想,扩展成主从读写分离,进一步提高数据库查询性能。

三、总结

本篇文章主要介绍了如何实现动态切换数据源信息,核心其实就是实现数据源路由配置,我们需要继承AbstractRoutingDataSource,并重写其中的determineCurrentLookupKey()方法,此方法从全局的数据源保存上下文中DataSourceContext类获取当前数据源,同时我们借助自定义注解和AOP实现,在AOP切面里面动态更新当前数据源信息到DataSourceContext类中。

本文动态切换数据源的方式只是数据库优化扩展的一个临时解决办法,仅仅适合数据量不是特别大的场景。随着系统负载进一步增大,此方案并不可行,小伙伴们可以根据具体的需求场景选择合适的方式来优化,如主从复制、读写分离、引入Mycat分库分表等等其他手段。

完整代码地址在Gitee:https://gitee.com/weixiaohuai/springboot-dynamic-datasource-demo.git

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页
实付 19.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值