Spring Boot + Mybatis 中 配置Druid多数据源并实现动态切换

概述

前面我们已经介绍过了对MyBatis、Druid的整合,接下来我们在之前的基础上做扩展,实现对Druid多数据源的配置以及动态切换数据源。

问题:多数据源使用场景有哪些呢?
回答:在业务发展中,数据的不断增长,会有读写分离的需求,以及按业务模块分库的需求,这样我们的数据源会越来越多,在项目中就有了在各个数据源之间来回切换的场景。

实践如何配置Druid多数据源并实现动态切换

  1. 首先是启动类的改造,在@SpringBootApplication注解后加上exclude = { DataSourceAutoConfiguration.class }
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
  1. yml配置文件,druid配置中,加入两个数据库,分别命名为master、slave
# MyBatis配置
mybatis:
  # 搜索指定包别名
  typeAliasesPackage: com.zhlab.demo.model
  # 配置mapper的扫描,找到所有的mapper.xml映射文件
  mapperLocations: classpath:mapper/*Mapper.xml
  # 加载全局的配置文件
  configLocation: classpath:mybatis/mybatis-config.xml


spring:
  ## 数据库配置
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    druid:
      master:
        url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: root
      slave:
        enabled: true
        url: jdbc:mysql://localhost:3306/demo2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
        username: root
        password: root
//...后边省略和之前一样
  1. 创建com.zhlab.demo.db包,并创建DataSourceType.java枚举类,存放所有数据源的名称
package com.zhlab.demo.db;

/**
 * @ClassName DataSourceType
 * @Description //DataSourceType 数据源类型
 * @Author singleZhang
 * @Email 405780096@qq.com
 * @Date 2020/11/2 0002 下午 2:41
 **/
public enum DataSourceType
{
    /**
     * master
     */
    MASTER,

    /**
     * slave
     */
    SLAVE
}

  1. 创建com.zhlab.demo.config.properties包,并创建DruidProperties.java类,用来获取连接池属性,并设置到数据源中
package com.zhlab.demo.config.properties;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

/**
 * @ClassName DruidProperties
 * @Description //DruidProperties
 * @Author singleZhang
 * @Email 405780096@qq.com
 * @Date 2020/11/2 0002 下午 2:44
 **/
@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);
        datasource.setValidationQuery(validationQuery);
        datasource.setTestWhileIdle(testWhileIdle);
        datasource.setTestOnBorrow(testOnBorrow);
        datasource.setTestOnReturn(testOnReturn);
        return datasource;
    }
}
  1. 接着配置MybatisConfig.java和DruidConfig.java两个配置类
    MybatisConfig
package com.zhlab.demo.config;

import org.apache.ibatis.io.VFS;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

/**
 * @ClassName MybatisConfig
 * @Description //MybatisConfig配置类
 * @Author singleZhang
 * @Email 405780096@qq.com
 * @Date 2020/10/31 0031 上午 9:37
 **/
@Configuration
@MapperScan("com.zhlab.demo.mapper") //mapper
public class MybatisConfig {

    @Autowired
    private Environment env;

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {

        String mapperLocations = env.getProperty("mybatis.mapperLocations");
        String configLocation = env.getProperty("mybatis.configLocation");
        VFS.addImplClass(SpringBootVFS.class);
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setTypeAliasesPackage("com.zhlab.demo.model");
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
        return sessionFactory.getObject();
    }

}

DruidConfig

package com.zhlab.demo.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.zhlab.demo.config.properties.DruidProperties;
import com.zhlab.demo.db.DataSourceType;
import com.zhlab.demo.db.datasource.DynamicDataSource;
import com.zhlab.demo.utils.SpringUtil;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
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 javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @ClassName DruidConfig
 * @Description //DruidConfig配置类
 * @Author singleZhang
 * @Email 405780096@qq.com
 * @Date 2020/11/2 0002 下午 2:08
 **/
@Configuration
public class DruidConfig {

    /**
     * master数据源
     * */
    @Bean
    @ConfigurationProperties("spring.datasource.druid.master")
    public DataSource masterDataSource(DruidProperties druidProperties)
    {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }

    /**
     * slave数据源
     * */
    @Bean
    @ConfigurationProperties("spring.datasource.druid.slave")
    @ConditionalOnProperty(prefix = "spring.datasource.druid.slave", name = "enabled", havingValue = "true")
    public DataSource slaveDataSource(DruidProperties druidProperties)
    {
        DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
        return druidProperties.dataSource(dataSource);
    }

    @Bean(name = "dynamicDataSource")
    @Primary
    public DynamicDataSource dataSource(DataSource masterDataSource) {

        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.MASTER.name(), masterDataSource);

        //设置备用
        setDataSource(targetDataSources, DataSourceType.SLAVE.name(), "slaveDataSource");
        return new DynamicDataSource(masterDataSource, targetDataSources);
    }

    /**
     * 设置数据源
     *
     * @param targetDataSources 备选数据源集合
     * @param sourceName 数据源名称
     * @param beanName bean名称
     */
    public void setDataSource(Map<Object, Object> targetDataSources, String sourceName, String beanName) {
        try {
            DataSource dataSource = SpringUtil.getBean(beanName);
            targetDataSources.put(sourceName, dataSource);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. 配置信息完成后,需要加入动态数据源支持,创建com.zhlab.demo.db.datasource包,并创建
    DynamicDataSource类,继承AbstractRoutingDataSource,这个抽象类有两个成员变量需要我们了解一下
  • targetDataSources:保存了key和数据库连接的映射关系
  • defaultTargetDataSource:表示默认的数据库连接
package com.zhlab.demo.db.datasource;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

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

/**
 * @ClassName DynamicDataSource
 * @Description //DynamicDataSource
 * @Author singleZhang
 * @Email 405780096@qq.com
 * @Date 2020/11/2 0002 下午 2:22
 **/
public class DynamicDataSource extends AbstractRoutingDataSource
{
    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources)
    {
        super.setDefaultTargetDataSource(defaultTargetDataSource);
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();

    }

    @Override
    protected Object determineCurrentLookupKey()
    {
        return DynamicDataSourceHelper.getDataSourceType();
    }
}

上述类中,重写了determineCurrentLookupKey()函数,我们看一下它在抽象类中是如何被使用的

    protected DataSource determineTargetDataSource() {
        Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
        Object lookupKey = this.determineCurrentLookupKey();
        DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);
        if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
            dataSource = this.resolvedDefaultDataSource;
        }

        if (dataSource == null) {
            throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
        } else {
            return dataSource;
        }
    }

所以,我们需要在determineCurrentLookupKey()方法中返回一个数据源的标志即可,即

    @Override
    protected Object determineCurrentLookupKey()
    {
        return DynamicDataSourceHelper.getDataSourceType();
    }
  1. 我们还需要创建一个自定义的DynamicDataSourceHelper类,来操作数据源的设置、获取和清除
package com.zhlab.demo.db.datasource;


/**
 * @ClassName DynamicDataSource
 * @Description //数据源切换处理
 * @Author singleZhang
 * @Email 405780096@qq.com
 * @Date 2020/11/2 0002 下午 2:22
 **/
public class DynamicDataSourceHelper
{

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

    /**
     * 设置数据源的变量
     */
    public static void setDataSourceType(String dsType) { CONTEXT_HOLDER.set(dsType); }

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

    /**
     * 清空数据源变量
     */
    public static void clearDataSourceType() {CONTEXT_HOLDER.remove();}
}
  1. 实现动态切换,通过注解来简化业务层的数据源切换,创建com.zhlab.demo.db.annotation包,并创建注解DataSource.java
package com.zhlab.demo.db.annotation;

import com.zhlab.demo.db.DataSourceType;

import java.lang.annotation.*;

/**
 * 自定义多数据源切换注解
 * 在这里切换数据源名称
 * */
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource
{
    DataSourceType value() default DataSourceType.MASTER;
}
  1. 使用AOP,切入DataSource注解,实现数据源切换,创建com.zhlab.demo.db.aspect包,并创建DynamicDataSourceAspect.java
package com.zhlab.demo.db.aspect;

import com.zhlab.demo.db.annotation.DataSource;

import com.zhlab.demo.db.datasource.DynamicDataSourceHelper;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * @ClassName DynamicDataSourceAspect
 * @Description //数据源动态切换AOP
 * @Author singleZhang
 * @Email 405780096@qq.com
 * @Date 2020/11/2 0002 下午 3:16
 **/
@Aspect
@Order(1)
@Component
public class DynamicDataSourceAspect {

    /**
     * 选择切入点为DataSource注解
     * */
    @Pointcut("@annotation(com.zhlab.demo.db.annotation.DataSource)"
            + "|| @within(com.zhlab.demo.db.annotation.DataSource)")
    public void dsPointCut() { }

    @Around("dsPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        DataSource dataSource = getDataSource(point);
        if (dataSource != null) {
            DynamicDataSourceHelper.setDataSourceType(dataSource.value().name());
        }

        try {
            return point.proceed();
        }
        finally {
            // 销毁数据源 在执行方法之后
            DynamicDataSourceHelper.clearDataSourceType();
        }
    }

    /**
     * 获取需要切换的数据源
     */
    public DataSource getDataSource(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Class<? extends Object> targetClass = point.getTarget().getClass();
        DataSource targetDataSource = targetClass.getAnnotation(DataSource.class);
        if (targetDataSource != null) {
            return targetDataSource;
        } else {
            Method method = signature.getMethod();
            DataSource dataSource = method.getAnnotation(DataSource.class);
            return dataSource;
        }
    }
}
  1. 完成以上的步骤后,可以在DAO层、Service层的方法中切换数据源了
package com.zhlab.demo.mapper;

import com.zhlab.demo.db.DataSourceType;
import com.zhlab.demo.db.annotation.DataSource;
import com.zhlab.demo.model.SysAdminUser;
import java.util.List;

public interface SysAdminUserMapper {
    int insert(SysAdminUser record);

    /**
     * 查询所有用户
     * */
    List<SysAdminUser> selectAll();
    
    //切换数据源后,查询所有用户
    @DataSource(value = DataSourceType.SLAVE)
    List<SysAdminUser> selectAll2();
}

SysAdminUserService.java

package com.zhlab.demo.service;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.zhlab.demo.db.DataSourceType;
import com.zhlab.demo.db.annotation.DataSource;
import com.zhlab.demo.mapper.SysAdminUserMapper;
import com.zhlab.demo.model.SysAdminUser;
import com.zhlab.demo.utils.PageUtil;
import com.zhlab.demo.utils.page.PageRequest;
import com.zhlab.demo.utils.page.PageResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @ClassName SysAdminUserService
 * @Description //SysAdminUserService
 * @Author singleZhang
 * @Email 405780096@qq.com
 * @Date 2020/10/31 0031 上午 9:45
 **/
@Service
public class SysAdminUserService {

    @Autowired
    SysAdminUserMapper sysAdminUserMapper;

    /**
     * 查询所有用户
     * */
    public List<SysAdminUser> findAll(){
        return sysAdminUserMapper.selectAll();
    }

    public List<SysAdminUser> findAll2(){
        return sysAdminUserMapper.selectAll2();
    }
}

UserController.java

package com.zhlab.demo.controller;

import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.zhlab.demo.model.SysAdminUser;
import com.zhlab.demo.service.SysAdminUserService;
import com.zhlab.demo.utils.PageUtil;
import com.zhlab.demo.utils.page.PageRequest;
import com.zhlab.demo.utils.page.PageResult;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * @ClassName UserController
 * @Description //用户接口层
 * @Author singleZhang
 * @Email 405780096@qq.com
 * @Date 2020/10/31 0031 上午 9:43
 **/
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    SysAdminUserService sysAdminUserService;

    /* 方法注解 */
    @ApiOperation(value = "方法名:用户列表", notes = "获取用户列表")
    @GetMapping("/list")
    public List<SysAdminUser> list(){
        List<SysAdminUser> list = sysAdminUserService.findAll();

        return list;
    }

    /* 方法注解 */
    @ApiOperation(value = "方法名:用户列表2", notes = "切换数据源获取用户列表")
    @GetMapping("/list2")
    public List<SysAdminUser> list2(){
        List<SysAdminUser> list = sysAdminUserService.findAll2();

        return list;
    }

}

demo2数据库中的数据需要和demo数据库中的数据不同,形成对比


demo2
demo
  1. 启动项目,打开http://localhost:8080/swagger-ui.html查看接口
    接口

    demo数据库,查询所有用户:/user/list
    demo

    demo2数据库,查询所有用户:/user/list2
    demo2

实现成功,完成动态切换数据源。

总结

Druid多数据源以及动态切换的使用场景其实在很多项目中是很常见的,需要大家掌握,以后接触到分布式系统的时候在这基础上会扩展得更多,需要持续深入研究。

项目地址

https://gitee.com/kaixinshow/springboot-note

返回【Spring Boot学习】目录

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,812评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,626评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,144评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,052评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,925评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,035评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,461评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,150评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,413评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,501评论 2 307
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,277评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,159评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,528评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,868评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,143评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,407评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,615评论 2 335