Spring Boot多數據源配置/讀寫分離(Druid+MyBatisPlus)

SpringBoot配置多數據源/讀寫分離整體步驟

  • Jar包引入 spring boot + druid + mybatis plus(多數據源 + 分頁
  • application.yml配置多數據源及mybatis plus mapper配置
  • 新建動態數據源DynamicDataSource(繼承AbstractRoutingDataSource),ThreadLocal中獲取當前使用哪個數據源
  • 自定義多個Datasource,枚舉類對應各個DataSource,及事務集成
  • 編寫AOP,增強Mapper方法的調用處,切換具體數據源
  • 測試,數據源切換、插入、分頁查詢等

項目源碼

https://github.com/zc-zangchao/multiple-data-source

POM配置

版本

        <springboot.version>2.2.0.RELEASE</springboot.version>
        <druid.version>1.1.9</druid.version>
        <oracle.version>12.1.0.1.0</oracle.version>
        <mysql-connector.version>8.0.18</mysql-connector.version>
        <mybatis-plus.version>2.1.9</mybatis-plus.version>
        <log4jdbc.version>1.2</log4jdbc.version>

具體jar

			<!--springboot-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>${springboot.version}</version>
            </dependency>
            
            <!--druid-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>${druid.version}</version>
            </dependency>
            <!-- 多數據源時使用mybatis-plus已經包含了mybatis+pageHelper,所以不需要再引用 mybatis + pageHelper -->
            <!-- 多數據源配置支撐 -->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>${mybatis-plus.version}</version>
            </dependency>

            <!-- log4jdbc jdbc日誌增強 -->
            <dependency>
                <groupId>com.googlecode.log4jdbc</groupId>
                <artifactId>log4jdbc</artifactId>
                <version>${log4jdbc.version}</version>
            </dependency>

            <!-- db connect -->
            <!-- oracle -->
            <dependency>
                <groupId>com.oracle</groupId>
                <artifactId>ojdbc7</artifactId>
                <version>${oracle.version}</version>
            </dependency>

            <!-- mysql -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql-connector.version}</version>
            </dependency>

yml配置

# mybatis mapper locations
mybatis-plus:
  mapper-locations: classpath:mapper/*Mapper.xml

spring:
  datasource:
    druid:
      oracle:
        url: jdbc:oracle:thin:@127.0.0.1:1521/db
        username: root
        password: root
        driver-class-name: oracle.jdbc.driver.OracleDriver
        max-active: 10
        max-wait: 10000
      mysql:
        # tcpRcvBuf/tcpSndBuf 緩衝區參數 rewriteBatchedStatements batchUpdate參數
        url: jdbc:log4jdbc:mysql://127.0.0.1:3306/mydb?allowMultiQueries=true&useUnicode=true&characterEncoding=utf-8&useSSL=false&tcpRcvBuf=1048576&tcpSndBuf=1048576&socketTimeout=180000&rewriteBatchedStatements=true&autoReconnect=true
        username: root
        password: root
        # 開源 SQL 日誌框架,在大多數情況下極大改善了可讀性及調試工作
        driver-class-name: net.sf.log4jdbc.DriverSpy
        max-active: 10
        max-wait: 600000
        # sql監控
        filters: stat
        # 檢測池裏連接的可用性 開啓影響性能 默認false
        test-on-borrow: false
        # 指明連接是否被空閒連接回收器(如果有)進行檢驗.如果檢測失敗,則連接將被從池中去除 默認true
        test-while-idle: true
        # 每30秒運行一次空閒連接回收器
        time-between-eviction-runs-millis: 30000
        # 檢測語句
        validation-query: "select 1"
      mysql-backup:
        # tcpRcvBuf/tcpSndBuf 緩衝區參數 rewriteBatchedStatements batchUpdate參數
        url: jdbc:mysql://127.0.0.1:3306/mydb?allowMultiQueries=true&useUnicode=true&characterEncoding=utf-8&useSSL=false
        username: root
        password: root
        driver-class-name: com.mysql.cj.jdbc.Driver
        max-active: 10
        max-wait: 600000
        # sql監控
        filters: stat
        # 檢測池裏連接的可用性 開啓影響性能 默認false
        test-on-borrow: false
        # 指明連接是否被空閒連接回收器(如果有)進行檢驗.如果檢測失敗,則連接將被從池中去除 默認true
        test-while-idle: true
        # 每30秒運行一次空閒連接回收器
        time-between-eviction-runs-millis: 30000
        # 檢測語句
        validation-query: "select 1"


動態數據源配置

package com.springboot.demo.service.datasource;

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

public class DynamicDataSource extends AbstractRoutingDataSource {

    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSource();
    }
}

package com.springboot.demo.service.datasource;

public class DataSourceContextHolder {
    private DataSourceContextHolder(){}

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

    public static void setDataSource(String dbType){
        contextHolder.set(dbType);
    }

    public static String getDataSource(){
        return contextHolder.get();
    }

    public static void clearDataSource(){
        contextHolder.remove();
    }
}

DataSource及事務配置

package com.springboot.demo.service.datasource.type;

public enum EnumDataSourceType {
    ORACLE,

    MYSQL,

    MYSQL_BACKUP;
}

package com.springboot.demo.service.datasource;

import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.baomidou.mybatisplus.MybatisConfiguration;
import com.baomidou.mybatisplus.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.spring.boot.starter.MybatisPlusProperties;
import com.baomidou.mybatisplus.spring.boot.starter.SpringBootVFS;
import com.springboot.demo.service.datasource.type.EnumDataSourceType;
import org.apache.ibatis.plugin.Interceptor;
import org.mybatis.spring.SqlSessionFactoryBean;
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.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

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

@Configuration
public class DataSourceConfig {

    @Resource
    private MybatisPlusProperties properties;

    @Bean(name = "dataSourceOracle")
    @Primary
    @ConfigurationProperties(prefix = "spring.datasource.druid.oracle")
    public DataSource oracleDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "dataSourceMysql")
    @ConfigurationProperties(prefix = "spring.datasource.druid.mysql")
    public DataSource mysqlDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "dataSourceMysqlBackup")
    @ConfigurationProperties(prefix = "spring.datasource.druid.mysql-backup")
    public DataSource mysqlBackupDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        // 配置默認數據源
        dynamicDataSource.setDefaultTargetDataSource(oracleDataSource());
        // 配置多數據源
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put(EnumDataSourceType.ORACLE.name(), oracleDataSource());
        dataSourceMap.put(EnumDataSourceType.MYSQL.name(), mysqlDataSource());
        dataSourceMap.put(EnumDataSourceType.MYSQL_BACKUP.name(), mysqlBackupDataSource());
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        return dynamicDataSource;
    }

    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        // 開啓 PageHelper 的支持
        paginationInterceptor.setLocalPage(true);
        return paginationInterceptor;
    }

    @Bean
    public SqlSessionFactoryBean sqlSessionFactoryBean() {

        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dynamicDataSource());
        // mybatis本身的核心庫在springboot打包成jar後有個bug,無法完成別名的掃描
        factory.setVfs(SpringBootVFS.class);

        org.apache.ibatis.session.Configuration configuration = this.properties.getConfiguration();
        if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) {
            configuration = new MybatisConfiguration();
        }

        factory.setConfiguration(configuration);

        // 分頁功能
        factory.setPlugins(new Interceptor[]{ paginationInterceptor()});

        if (this.properties.getConfigurationProperties() != null) {
            factory.setConfigurationProperties(this.properties.getConfigurationProperties());
        }

        if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
            factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
        }

        if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
            factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
        }

        if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
            factory.setMapperLocations(this.properties.resolveMapperLocations());
        }

        return factory;
    }

//    @Bean
//    public SqlSessionFactory sqlSessionFactory() throws Exception {
//        MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
//        sqlSessionFactory.setDataSource(dynamicDataSource());
//        sqlSessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*Mapper.xml"));
//
//        MybatisConfiguration configuration = new MybatisConfiguration();
//        configuration.setJdbcTypeForNull(JdbcType.NULL);
//        configuration.setMapUnderscoreToCamelCase(true);
//        configuration.setCacheEnabled(false);
//        sqlSessionFactory.setConfiguration(configuration);
//        sqlSessionFactory.setPlugins(new Interceptor[]{
//                paginationInterceptor() //添加分頁功能
//        });
//
//        return sqlSessionFactory.getObject();
//    }

    /**
     * 配置@Transactional註解事務
     */
    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dynamicDataSource());
    }

}

AOP增強 切換數據源

package com.springboot.demo.service.datasource.annotation;

import com.springboot.demo.service.datasource.type.EnumDataSourceType;

import java.lang.annotation.*;

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TargetDataSource {

    EnumDataSourceType value() default EnumDataSourceType.ORACLE;

}

package com.springboot.demo.service.datasource.aspect;

import com.springboot.demo.service.datasource.DataSourceContextHolder;
import com.springboot.demo.service.datasource.annotation.TargetDataSource;
import com.springboot.demo.service.datasource.type.EnumDataSourceType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Component
@Aspect
@Slf4j
// 默認優先級 最後執行 可調整Order
public class DynamicDataSourceAspect {


    @Before("@annotation(targetDataSource)")
    public void before(JoinPoint point, TargetDataSource targetDataSource) {
        try {
            TargetDataSource annotationOfClass = point.getTarget().getClass().getAnnotation(TargetDataSource.class);
            String methodName = point.getSignature().getName();
            Class[] parameterTypes = ((MethodSignature) point.getSignature()).getParameterTypes();
            Method method = point.getTarget().getClass().getMethod(methodName, parameterTypes);
            TargetDataSource methodAnnotation = method.getAnnotation(TargetDataSource.class);
            methodAnnotation = methodAnnotation == null ? annotationOfClass : methodAnnotation;
            EnumDataSourceType dataSourceType = methodAnnotation != null && methodAnnotation.value() != null ? methodAnnotation.value() : EnumDataSourceType.ORACLE;
            DataSourceContextHolder.setDataSource(dataSourceType.name());
        } catch (NoSuchMethodException e) {
            log.warn("Aspect targetDataSource inspect exception.", e);
        }
    }

    @After("@annotation(targetDataSource)")
    public void after(JoinPoint point, TargetDataSource targetDataSource) {
        DataSourceContextHolder.clearDataSource();
    }
}

測試驗證

驗證數據源切換、事務、分頁

package com.springboot.demo.web.service;

import com.baomidou.mybatisplus.plugins.pagination.PageHelper;
import com.springboot.demo.service.dao.domain.User;
import com.springboot.demo.service.dao.mapper.UserMapper;
import com.springboot.demo.service.datasource.annotation.TargetDataSource;
import com.springboot.demo.service.datasource.type.EnumDataSourceType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.List;

@Service
public class UserService {
    @Resource
    private UserMapper userMapper;

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public boolean addUser(User user) {
        int result = userMapper.insert(user);
        if (result <= 0) {
            throw new RuntimeException("exception");
        }
        return result > 0;
    }

    @TargetDataSource(EnumDataSourceType.MYSQL_BACKUP)
    public User queryUserById(String userId) {
        return userMapper.selectByPrimaryKey(userId);
    }

    @TargetDataSource(EnumDataSourceType.MYSQL)
    public List<User> selectAll(int pageNum, int pageSize) {
        // 啓動分頁
        PageHelper.startPage(pageNum, pageSize);
        return userMapper.selectAll();
    }
}

package com.springboot.demo.web.controller;

import com.springboot.demo.service.dao.domain.User;
import com.springboot.demo.web.model.ResultInfo;
import com.springboot.demo.web.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.websocket.server.PathParam;
import java.util.List;

@RestController
@Slf4j
public class UserController {
    @Resource
    private UserService userService;

    @GetMapping("/user/query")
    public List<User> queryUsers(@PathParam("pageNum") int pageNum, @PathParam("pageSize") int pageSize) {
        log.info("Query users.");
        return userService.selectAll(pageNum, pageSize);
    }

    @GetMapping("/user/queryUserById/{userId}")
    public User queryUserById(@PathVariable String userId) {
        log.info("Query user by Id.");
        return userService.queryUserById(userId);
    }

    @PostMapping("/user/add")
    public ResultInfo addUser(@RequestBody User user) {
        log.info("Add user.");
        userService.addUser(user);
        return new ResultInfo();
    }
}



參考:
Spring Boot 整合 Durid數據庫連接池
Spring Boot2.0配置Druid數據庫連接池(單數據源、多數據源、數據監控)
使用springboot + druid + mybatisplus完成多數據源配置
數據連接池默認配置帶來的坑testOnBorrow=false,cloes_wait 終於解決了
關於連接池參數testWhileIdle,testOnBorrow,testOnReturn的疑問
使用druid連接池帶來的坑testOnBorrow=false
MySQL之rewriteBatchedStatements
使用log4jdbc更有效的記錄java sql日誌

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章