SpringBoot+MybatisPlus多數據源配置,主從庫讀寫分離完整講解

前言

參考文章:基於Mybatis框架,採用切面編程方式實現
     https://www.jianshu.com/p/2222257f96d3
以上,包括其他技術文章,實現方式都是大同小異,都存在着一些小問題。譬如:

  • 採用切面編程,字符串匹配方式,侷限於方法寫死,特別是項目處於開發收尾的話,改動代碼量大。
  • 以註解的方式類同。
    本篇優勢:
  • 源代碼不變(通過mybatis攔截器),易擴展
  • 多數據源可配置

示例圖

這裏寫圖片描述
這裏寫圖片描述

導包

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.42</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.42</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatisplus-spring-boot-starter</artifactId>
            <version>1.0.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

配置文件application.yaml

spring:  
  datasource:
    slave:
      url: jdbc:mysql://192.168.1.70:3306/test?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
      username: kmob
      password: kmob0724(
      filters: mergeStat
      on-off: true
      driver-class-name: com.mysql.jdbc.Driver
    master:
      url: jdbc:mysql://**.**.**.**:3306/test?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false
      username: **
      password: ****
      filters: mergeStat
      driver-class-name: com.mysql.jdbc.Driver

mybatis-plus:
  mapper-locations: classpath*:com/example/demo/**/mapping/*.xml
  typeAliasesPackage: com.example.demo.entity 
  typeHandlersPackage: com.example.demo.entity.config
  global-config:
    id-type: 3
    db-column-underline: false
    refresh-mapper: true
    is-capital-mode: false
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: true
    lazyLoadingEnabled: true
    jdbcTypeForNull: null
    multipleResultSetsEnabled: true

on-off用於配置是否啓動多數據源。

package com.example.demo.config;

import java.sql.SQLException;

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

import com.alibaba.druid.pool.DruidDataSource;

/**
 * <p>數據庫數據源配置</p>
 * <p>說明:這個類中包含了許多默認配置,若這些配置符合您的情況,您可以不用管,若不符合,建議不要修改本類,建議直接在"application.yml"中配置即可</p>
 * 
 */
@Component
@ConfigurationProperties(prefix = "spring.datasource.slave")
public class DruidProperties {

    private String url = "jdbc:mysql://127.0.0.1:3306/operation?autoReconnect=true&useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull";

    private String username = "root";

    private String password = "admin";

    private String driverClassName = "com.mysql.jdbc.Driver";

    private Integer initialSize = 2;

    private Integer minIdle = 1;

    private Integer maxActive = 20;

    private Integer maxWait = 60000;

    private Integer timeBetweenEvictionRunsMillis = 60000;

    private Integer minEvictableIdleTimeMillis = 300000;

    private String validationQuery = "SELECT 'x' from dual";

    private Boolean testWhileIdle = true;

    private Boolean testOnBorrow = false;

    private Boolean testOnReturn = false;

    private Boolean poolPreparedStatements = true;

    private Integer maxPoolPreparedStatementPerConnectionSize = 20;

    private String filters = "stat";

    private Boolean onOff = false;

    public void coinfig(DruidDataSource dataSource) {

        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);

        dataSource.setDriverClassName(driverClassName);
        dataSource.setInitialSize(initialSize);     //定義初始連接數
        dataSource.setMinIdle(minIdle);             //最小空閒
        dataSource.setMaxActive(maxActive);         //定義最大連接數
        dataSource.setMaxWait(maxWait);             //最長等待時間

        // 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連接,單位是毫秒
        dataSource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);

        // 配置一個連接在池中最小生存的時間,單位是毫秒
        dataSource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        dataSource.setValidationQuery(validationQuery);
        dataSource.setTestWhileIdle(testWhileIdle);
        dataSource.setTestOnBorrow(testOnBorrow);
        dataSource.setTestOnReturn(testOnReturn);

        // 打開PSCache,並且指定每個連接上PSCache的大小
        dataSource.setPoolPreparedStatements(poolPreparedStatements);
        dataSource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);

        try {
            dataSource.setFilters(filters);
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getDriverClassName() {
        return driverClassName;
    }

    public void setDriverClassName(String driverClassName) {
        this.driverClassName = driverClassName;
    }

    public Integer getInitialSize() {
        return initialSize;
    }

    public void setInitialSize(Integer initialSize) {
        this.initialSize = initialSize;
    }

    public Integer getMinIdle() {
        return minIdle;
    }

    public void setMinIdle(Integer minIdle) {
        this.minIdle = minIdle;
    }

    public Integer getMaxActive() {
        return maxActive;
    }

    public void setMaxActive(Integer maxActive) {
        this.maxActive = maxActive;
    }

    public Integer getMaxWait() {
        return maxWait;
    }

    public void setMaxWait(Integer maxWait) {
        this.maxWait = maxWait;
    }

    public Integer getTimeBetweenEvictionRunsMillis() {
        return timeBetweenEvictionRunsMillis;
    }

    public void setTimeBetweenEvictionRunsMillis(Integer timeBetweenEvictionRunsMillis) {
        this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
    }

    public Integer getMinEvictableIdleTimeMillis() {
        return minEvictableIdleTimeMillis;
    }

    public void setMinEvictableIdleTimeMillis(Integer minEvictableIdleTimeMillis) {
        this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
    }

    public String getValidationQuery() {
        return validationQuery;
    }

    public void setValidationQuery(String validationQuery) {
        this.validationQuery = validationQuery;
    }

    public Boolean getTestWhileIdle() {
        return testWhileIdle;
    }

    public void setTestWhileIdle(Boolean testWhileIdle) {
        this.testWhileIdle = testWhileIdle;
    }

    public Boolean getTestOnBorrow() {
        return testOnBorrow;
    }

    public void setTestOnBorrow(Boolean testOnBorrow) {
        this.testOnBorrow = testOnBorrow;
    }

    public Boolean getTestOnReturn() {
        return testOnReturn;
    }

    public void setTestOnReturn(Boolean testOnReturn) {
        this.testOnReturn = testOnReturn;
    }

    public Boolean getPoolPreparedStatements() {
        return poolPreparedStatements;
    }

    public void setPoolPreparedStatements(Boolean poolPreparedStatements) {
        this.poolPreparedStatements = poolPreparedStatements;
    }

    public Integer getMaxPoolPreparedStatementPerConnectionSize() {
        return maxPoolPreparedStatementPerConnectionSize;
    }

    public void setMaxPoolPreparedStatementPerConnectionSize(Integer maxPoolPreparedStatementPerConnectionSize) {
        this.maxPoolPreparedStatementPerConnectionSize = maxPoolPreparedStatementPerConnectionSize;
    }

    public String getFilters() {
        return filters;
    }

    public void setFilters(String filters) {
        this.filters = filters;
    }

    public Boolean getOnOff() {
        return onOff;
    }

    public void setOnOff(Boolean onOff) {
        this.onOff = onOff;
    }

}

區分數據源

package com.example.demo.dynamic;

public enum DatabaseType {

    master("write"),slave("read");

    private DatabaseType(String name) {
        this.name = name();
    }

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }



}

記錄當前線程數據源

package com.example.demo.dynamic;

public class DatabaseContextHolder {

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

    public static void setDatabaseType(DatabaseType type) {
        contextHolder.set(type);
    }

    public static DatabaseType getDatabaseType() {
        return contextHolder.get();
    }

}
package com.example.demo.dynamic;

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

public class DynamicDataSource extends AbstractRoutingDataSource {


    @Override
    protected Object determineCurrentLookupKey() {
        DatabaseType type = DatabaseContextHolder.getDatabaseType();

        if(type == null) {
            logger.info("========= dataSource ==========" + DatabaseType.slave.name());
            return DatabaseType.slave.name();
        }

        logger.info("========= dataSource ==========" + type);
        return type;
    }

}

攔截器

package com.example.demo.dynamic;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;

@Intercepts({
@Signature(type = Executor.class, method = "update", args = {
        MappedStatement.class, Object.class }),
@Signature(type = Executor.class, method = "query", args = {
        MappedStatement.class, Object.class, RowBounds.class,
        ResultHandler.class }) })
public class DatabasePlugin implements Interceptor {

    protected static final Logger logger = LoggerFactory.getLogger(DatabasePlugin.class);

    private static final String REGEX = ".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*";

    private static final Map<String, DatabaseType> cacheMap = new ConcurrentHashMap<>();

    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
        if(!synchronizationActive) {
            Object[] objects = invocation.getArgs();
            MappedStatement ms = (MappedStatement) objects[0];

            DatabaseType databaseType = null;

            if((databaseType = cacheMap.get(ms.getId())) == null) {
                //讀方法
                if(ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
                    //!selectKey 爲自增id查詢主鍵(SELECT LAST_INSERT_ID() )方法,使用主庫
                    if(ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
                        databaseType = DatabaseType.master;
                    } else {
                        BoundSql boundSql = ms.getSqlSource().getBoundSql(objects[1]);
                        String sql = boundSql.getSql().toLowerCase(Locale.CHINA).replaceAll("[\\t\\n\\r]", " ");
                        if(sql.matches(REGEX)) {
                            databaseType = DatabaseType.master;
                        } else {
                            databaseType = DatabaseType.slave;
                        }
                    }
                }else{
                    databaseType = DatabaseType.master;
                }
                logger.warn("設置方法[{}] use [{}] Strategy, SqlCommandType [{}]..", ms.getId(), databaseType.name(), ms.getSqlCommandType().name());
                cacheMap.put(ms.getId(), databaseType);
            }
            DatabaseContextHolder.setDatabaseType(databaseType);
        }

        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties properties) {
        // TODO Auto-generated method stub

    }

}

通過正則表達式,攔截sql語句匹配類型設置數據源。用map對象緩存數據,數據量大的話,此處需要優化。

數據源配置

package com.example.demo.config;

import java.util.HashMap;
import java.util.Map;

import javax.sql.DataSource;

import org.apache.ibatis.plugin.Interceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
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 com.alibaba.druid.pool.DruidDataSource;
import com.baomidou.mybatisplus.enums.DBType;
import com.baomidou.mybatisplus.plugins.PaginationInterceptor;
import com.baomidou.mybatisplus.spring.MybatisSqlSessionFactoryBean;
import com.example.demo.dynamic.DatabasePlugin;
import com.example.demo.dynamic.DatabaseType;
import com.example.demo.dynamic.DynamicDataSource;

/**
 * MybatisPlus 配置
 * 
 *   
 * @author fengjk  
 * @date 2017年10月17日
 * @since 1.0
 */
@Configuration
@MapperScan(basePackages = {"com.example.demo.dao"})
public class MybatisPlusConfig {

    @Autowired
    DruidProperties druidProperties;

    /**
     * mybatis-plus分頁插件
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
        paginationInterceptor.setDialectType(DBType.MYSQL.getDb());
        return paginationInterceptor;
    }

    @Bean(name = "masterDataSource")
    @Qualifier("masterDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "slaveDataSource")
    @Qualifier("slaveDataSource")
    public DataSource slaveDataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        druidProperties.coinfig(dataSource);

        return dataSource;
    }

    /**
     *  構造多數據源連接池
     *  Master 數據源連接池採用 HikariDataSource
     *  Slave  數據源連接池採用 DruidDataSource
     * @param master
     * @param slave
     * @return
     */
    @Bean
    @Primary
    public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource master,
                                        @Qualifier("slaveDataSource") DataSource slave) {
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DatabaseType.master, master);
        targetDataSources.put(DatabaseType.slave, slave);

        DynamicDataSource dataSource = new DynamicDataSource();
        dataSource.setTargetDataSources(targetDataSources);// 該方法是AbstractRoutingDataSource的方法
        dataSource.setDefaultTargetDataSource(slave);// 默認的datasource設置爲myTestDbDataSourcereturn dataSource;
        return dataSource;
    }

    @Bean
    public MybatisSqlSessionFactoryBean sqlSessionFactory(@Qualifier("masterDataSource") DataSource master,
                                               @Qualifier("slaveDataSource") DataSource slave) throws Exception {
        MybatisSqlSessionFactoryBean fb = new MybatisSqlSessionFactoryBean();
        fb.setDataSource(this.dataSource(master, slave));
        // 是否啓動多數據源配置,目的是方便多環境下在本地環境調試,不影響其他環境
        if (druidProperties.getOnOff() == true) {
            fb.setPlugins(new Interceptor[]{new DatabasePlugin()});
        }
        return fb;
    }

}

如果是用Mybatis框架,把MybatisSqlSessionFactoryBean修改成SqlSessionFactoryBean就行,具體參考前言提到的文章。

測試

@Controller
@RequestMapping("/user")
public class UserController {

    @Autowired
    UserMapper userMapper;

    @PostMapping("/insert")
    @ResponseBody
    public JSONResult insert(User user){
        user.setId(IdWorker.getId());
        userMapper.insert(user);

        return JSONResult.ok(user);
    }

    @PostMapping("/update")
    @ResponseBody
    public JSONResult update(User user){
        userMapper.updateById(user);

        return JSONResult.ok(user);
    }

    @PostMapping("/delete")
    @ResponseBody
    public JSONResult delete(long id){
        userMapper.deleteById(id);

        return JSONResult.ok(id);
    }

    @GetMapping("/select")
    @ResponseBody
    public JSONResult select(){
        List<User> userLists = userMapper.selectList(new EntityWrapper<User>());

        return JSONResult.ok(userLists);
    }

    @GetMapping("/selectCount")
    @ResponseBody
    public JSONResult selectCount(){
        int count = userMapper.selectCount(new EntityWrapper<User>());

        return JSONResult.ok(count);
    }

}

其他類就不曬代碼了,自行下載代碼。

總結

大部分細節代碼都沒做講解,有些東西講多了真沒意思,還是要靠自己去理解(參考上文鏈接文章)。最後,文章如有筆誤,請留言相告,謝謝。歡迎加Q羣交流學習:583138104
代碼下載

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