Spring Boot + Mybatis + Druid 動態切換多數據源


在大型應用程序中,配置主從數據庫並使用讀寫分離是常見的設計模式。

在Spring應用程序中,要實現讀寫分離,最好不要對現有代碼進行改動,而是在底層透明地支持。

這樣,就需要我們再一個項目中,配置兩個,乃至多個數據源。

今天,小編先來介紹一下自己配置動態多數據源的步驟

項目簡介:

  編譯器:IDEA

  JDK:1.8

  框架:Spring Boot 2.1.0.RELEASES  + Mybatis + Druid

一、配置數據庫連接數據

因為項目使用的是Spring Boot 框架,該框架會自動配置數據源,自動從application.properties中讀取數據源信息,如果沒有配置,啟動時會報錯,因此我們再配置自定義的數據源的時候,需要禁掉數據源的自動配置。

但是小編在啟動項目的時候,還是報錯了,可是由於jdbcTemplate重復了,框架自動幫我們定義了一個jdbcTemplate,而小編自己又自定義了一個,因此,也要將這個自動配置禁止掉

啟動類方法如下:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,JdbcTemplateAutoConfiguration.class})
@MapperScan(sqlSessionTemplateRef = "jdbcTemplate")
public class DynamicDatasourseApplication {

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

下面開始配置自定義的數據源。

新建jdbc.properties文件,配置數據庫的連接,數據源1為寫庫,數據源2為讀庫

jdbc.driverClassName.db=com.mysql.jdbc.Driver
#主數據源
jdbc.w.url=jdbc:mysql://localhost:3306/learning?characterEncoding=UTF-8&&serverTimezone=UTC
jdbc.w.user=root
jdbc.w.password=123456
#從數據源
jdbc.r.url=jdbc:mysql://localhost:3306/slave?characterEncoding=UTF-8&&serverTimezone=UTC
jdbc.r.user=root
jdbc.r.password=123456
#連接池配置
druid.initialSize=2
druid.minIdle=30
druid.maxActive=80
druid.maxWait=60000
druid.timeBetweenEvictionRunsMillis=60000
druid.minEvictableIdleTimeMillis=300000
druid.validationQuery=SELECT 'x'
druid.testWhileIdle=true
druid.testOnBorrow=false
druid.testOnReturn=false
druid.poolPreparedStatements=true
druid.maxPoolPreparedStatementPerConnectionSize=20
druid.filters=wall,stat

建表語句:

#數據庫learning
CREATE TABLE `a`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `age` int(11) NOT NULL,
  `gender` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `psw` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `seq` int(11) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;


INSERT INTO `a` VALUES (1, 'zsan', 30, 'f', '123456', 3);
INSERT INTO `a` VALUES (2, 'lisi', 31, 'f', '123456', 5);
INSERT INTO `a` VALUES (3, 'wangwu', 32, 'm', '123456', 1);
INSERT INTO `a` VALUES (4, 'zhaoliu', 33, 'm', '123456', 4);
INSERT INTO `a` VALUES (5, 'baiqi', 34, 'm', '123456', 6);
INSERT INTO `a` VALUES (6, 'hongba', 35, 'f', '123456', 2);
INSERT INTO `a` VALUES (7, 'zhuyl', 30, 'f', '123456', 7);

#數據庫slave
CREATE TABLE `b`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `age` int(11) NOT NULL,
  `gender` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `psw` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `seq` int(11) NOT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;


INSERT INTO `b` VALUES (1, 'szsan', 30, 'f', '123456', 3);
INSERT INTO `b` VALUES (2, 'slisi', 31, 'f', '123456', 5);
INSERT INTO `b` VALUES (3, 'swangwu', 32, 'm', '123456', 1);
INSERT INTO `b` VALUES (4, 'szhaoliu', 33, 'm', '123456', 4);
INSERT INTO `b` VALUES (5, 'sbaiqi', 34, 'm', '123456', 6);
INSERT INTO `b` VALUES (6, 'shongba', 35, 'f', '123456', 2);
INSERT INTO `b` VALUES (7, 'szhuyl', 30, 'f', '123456', 7);
建表語句

二、配置mybatis的屬性

在application.properties中配置mybatis的屬性

mybatis.type-aliases-package:實體類的位置,如果將實體類放到Application.java文件的同級包或者下級包時,這個屬性可以不配置
mybatis.mapper-locations:mapper.xml的位置
mybatis.config-location:mybatis配置文件的位置,無則不填
mybatis.type-aliases-package=cn.com.exercise.dynamicDatasourse.module.condition
mybatis.mapper-locations=/mappers/**.xml
mybatis.config-location=/config/sqlmap-config.xml

三、使用Java文件讀取資源數據

1)配置主數據源(寫庫)

    @Bean(name = '寫庫名字')
    @Primary
    public DataSource master(){
        DruidDataSource source = new DruidDataSource();
        //使用source.setXxx(Yyy);進行配置
        //數據庫基本屬性driverClassName url、user、password配置
        //連接池基本屬性配置
        return source;
    }    

@Primary表示優先為注入的Bean,此處用來標識住數據源

2)配置從數據源(讀庫),配置內容和主數據源相同

    @Bean(name = '讀庫名字')
    public DataSource master(){
        DruidDataSource source = new DruidDataSource();
        //使用source.setXxx(Yyy);進行配置
        //數據庫基本屬性driverClassName url、user、password配置
        //連接池基本屬性配置
        return source;
    } 

3)數據源支持,配置默認數據源

    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource(){
        DynamicDataSource dynamicRoutingDataSource = new DynamicDataSource();
        //配置多數據源
        Map<Object, Object> dataSourceMap = new HashMap<>(2);
        dataSourceMap.put("寫庫名字", master());
        dataSourceMap.put("讀庫名字", slave());
        // 將 master 數據源作為默認指定的數據源
        dynamicRoutingDataSource.setDefaultTargetDataSource(master());
        // 將 master 和 slave 數據源作為指定的數據源
        dynamicRoutingDataSource.setTargetDataSources(dataSourceMap);
        return dynamicRoutingDataSource;
    }

4)配置sqlSessionFactory和jdbcTemplate

在sqlSessionFactory中,配置mybatis相關的三個內容:typeAliasesPackage,configLocation和mapperLocation,分別對應了application.properties中的三個內容,有則配置,無則省略。

    @Bean(name = "sqlSessionFactory")
    public SqlSessionFactoryBean sqlSessionFactoryBean() throws Exception{
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setVfs(SpringBootVFS.class);
        sqlSessionFactoryBean.setTypeAliasesPackage(typeAlias);
        sqlSessionFactoryBean.setConfigLocation( new ClassPathResource(sqlmapConfigPath));
        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        String packageSearchPath = PathMatchingResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX+mapperLocation;
        sqlSessionFactoryBean.setMapperLocations(resolver.getResources(packageSearchPath));
        sqlSessionFactoryBean.setDataSource(dynamicDataSource());
        return  sqlSessionFactoryBean;
    }

    @Bean(name = "jdbcTemplate")
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory){
        return new SqlSessionTemplate(sqlSessionFactory);
    }

5)配置事務傳播相關內容

    @Bean(name = "transactionManager")
    public PlatformTransactionManager transactionManager() {
        DataSourceTransactionManager manager = new DataSourceTransactionManager(dynamicDataSource());
        return manager;
    }

    /**
     * 配置事務的傳播特性
     */
    @Bean(name = "txAdvice")
    public TransactionInterceptor txAdvice(){
        TransactionInterceptor interceptor = new TransactionInterceptor();
        interceptor.setTransactionManager(transactionManager());
        Properties transactionAttributes = new Properties();
        //使用transactionAttributes.setProperty()配置傳播特性
        interceptor.setTransactionAttributes(transactionAttributes);
        return interceptor;
    }

    @Bean(name = "txAdviceAdvisor")
    public Advisor txAdviceAdvisor() {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        String transactionExecution = "execution(* cn.com.hiveview.springboot.demoapi..service.*.*(..))";
        pointcut.setExpression(transactionExecution);
        return new DefaultPointcutAdvisor(pointcut, txAdvice());
    }

四、動態數據源支持

在上面配置動態數據源支持的時候,我們使用了一個類“DynamicDataSource.java”。

這個類是自定義的類,繼承了抽象類AbstractRoutingDataSource,正是通過這個抽象類來實現動態數據源的選擇的。

來看下這個抽象類的成員變量:

private Map<Object, Object> targetDataSources;
private Object defaultTargetDataSource;

以下介紹可以參考上一節(3)的內容。

【1】targetDataSources:保存了key和數據庫連接的映射關系

【2】defaultTargetDataSource:表示默認的數據庫連接

接下來就是根據這個類,實現我們自己的類DynamicDataSource.java

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Autowired
    private DBHelper helper;
    
    @Override
    protected Object determineCurrentLookupKey() {
        return helper.getDBType();
    }
}

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方法中,返回數據庫的標志即可。

DBHelper類也是自定義的類,數據源持有類,存放了讀、寫庫名字,以及設置數據源類型、獲取數據源類型、清除數據源類型的方法

@Component
public class DBHelper {
     /** 
     * 線程獨立 
     */  
    private ThreadLocal<String> contextHolder = new ThreadLocal<String>();  
    
    public static final String DB_TYPE_RW = "dataSource_db01";
    public static final String DB_TYPE_R = "dataSource_db02";
  
    public String getDBType() {
        String db = contextHolder.get();  
        if (db == null) {
            db = DB_TYPE_RW;
            // 默認是讀寫庫
        }
        return db;  
    }
  
    public void setDBType(String str) {
        contextHolder.set(str);
    }
   
    public void clearDBType() {  
        contextHolder.remove();  
    } 
}

五、動態切換

1)配置注解 DS.java

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
public @interface DS {
    String value() default "主庫名字";
}

2)使用AOP切換

@Aspect
@Component
@Order(0)
public class DynamicDataSourceAspect {
    private static final Logger logger = LoggerFactory.getLogger(DynamicDataSourceAspect.class.getName());

    @Autowired
    DBHelper dbHelper;

    /**
     * 在Mapper層添加注解,實現切換數據源
     */
    @Pointcut("execution(* cn.com.exercise.dynamicDatasourse.module..mapper.*.*(..))")
    public void dataSourcePointCut(){
    }

    @Before("dataSourcePointCut()")
    public void before(JoinPoint joinPoint) {
        Object target = joinPoint.getTarget();
        String method = joinPoint.getSignature().getName();
        Class<?>[] clazz = target.getClass().getInterfaces();
        Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
        try {
            Method m = clazz[0].getMethod(method, parameterTypes);
            //如果方法上存在切換數據源的注解,則根據注解內容進行數據源切換
            if (m != null && m.isAnnotationPresent(DS.class)) {
                DS data = m.getAnnotation(DS.class);
                String dataSourceName = data.value();
                dbHelper.setDBType(dataSourceName);
                logger.debug("current thread " + Thread.currentThread().getName() + " add " + dataSourceName + " to ThreadLocal");
            } else {
                logger.debug("switch datasource fail,use default");
            }
        } catch (Exception e) {
            logger.error("current thread " + Thread.currentThread().getName() + " add data to ThreadLocal error", e);
        }
    }

    @After("dataSourcePointCut()")
    public void after(JoinPoint joinPoint){
        dbHelper.clearDBType();
    }
}

完成以上內容后,就可以在mapper層的方法上,添加@DS注解,來實現數據源的切換了。

六、使用

mapper層代碼

@Mapper
public interface DynamicMapper {
    List<DynamicCondition> getListFromSource1();

    @DS(DBHelper.DB_TYPE_R)
    List<DynamicCondition> getListFromSource2();
}

由於寫庫是默認數據源,因此當不使用@DS配置數據源,以及使用@DS(“寫庫名字”)時,使用的都是寫庫。

依次訪問地址:

http://localhost:8082/dynamic/source1,

 http://localhost:8082/dynamic/source2

運行結果如下:

 

按照以上步驟,就可以完成動態切換數據源了,下面附上 完整代碼連接


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM