Spring-Blog:個人博客(一)-Mybatis 讀寫分離


概述:

  2018,在平(tou)靜(lan)了一段時間后,開始找點事情來做。這一次准備開發一個個人博客,在開發過程之中完善一下自己的技術。本系列博客只會提出一些比較有價值的技術思路,不會像寫流水賬一樣記錄開發過程。

  技術棧方面,會采用Spring Boot 2.0 作為底層框架,主要為了后續能夠接入Spring Cloud 進行學習拓展。並且Spring Boot 2.0基於Spring5,也可以提前預習一些Spring5的新特性。后續技術會在相應博客中提出。

  項目GitHub地址:https://github.com/jaycekon/Spring-Blog

  介紹一下目錄結構:

  • Spring-Blog( Parent 項目)
  • Spring-Blog-common( Util 模塊)
  • Spring-Blog-business(Repository模塊)
  • Spring-Blog-api (Web 模塊)
  • Spring-Blog-webflux (基於Spring Boot 2.0Web模塊)

 

  為了讓各位朋友能夠更好理解這一模塊的內容,演示代碼將存放在Spring Boot 項目下:

  Github 地址:https://github.com/jaycekon/SpringBoot

 

1、DataSource

    在開始講解前,我們需要先構建后我們的運行環境。Spring Boot 引入 Mybatis 的教程 可以參考 傳送門 。這里我們不細述了,首先來看一下我們的目錄結構:

 

  有使用過Spring Boot 的童鞋應該清楚,當我們在application.properties 配置好了我們的數據庫連接信息后,Spring Boot 將會幫我們自動裝載好 DataSource 。但如果我們需要進行讀寫分離操作是,如何配置自己的數據源,是我們必須掌握的。

  首先我們來看一下配置文件中的信息:

spring.datasource.url=jdbc:mysql://localhost:3306/charles_blog2
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

#別名掃描目錄
mybatis.type-aliases-package=com.jaycekon.demo.model
#Mapper.xml掃描目錄
mybatis.mapper-locations=classpath:mybatis-mappers/*.xml

#tkmapper 幫助工具
mapper.mappers=com.jaycekon.demo.MyMapper
mapper.not-empty=false
mapper.identity=MYSQL

  

1.1 DataSourceBuilder

      我們首先來看一下使用 DataSourceBuilder 來構建出DataSource:

@Configuration
@MapperScan("com.jaycekon.demo.mapper")
@EnableTransactionManagement
public class SpringJDBCDataSource {

    /**
     * 通過Spring JDBC 快速創建 DataSource
     * 參數格式
     * spring.datasource.master.jdbcurl=jdbc:mysql://localhost:3306/charles_blog
     * spring.datasource.master.username=root
     * spring.datasource.master.password=root
     * spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver
     *
     * @return DataSource
     */
    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource dataSource() {
        return DataSourceBuilder.create().build();
    }
}

      從代碼中我們可以看出,使用DataSourceBuilder 構建DataSource 的方法非常簡單,但是需要注意的是:

  •     DataSourceBuilder 只能自動識別配置文件中的 jdbcurl,username,password,driver-class-name等命名,因此我們需要在方法體上加上 @ ConfigurationProperties 注解。
  •          數據庫連接地址變量名需要使用 jdbcurl
  •               數據庫連接池使用 com.zaxxer.hikari.HikariDataSource

    執行單元測試時,我們可以看到 DataSource 創建以及關閉的過程。

 

1.2 DruidDataSource

    除了使用上述的構建方法外,我們可以選擇使用阿里提供的 Druid 數據庫連接池創建 DataSource

@Configuration
@EnableTransactionManagement
public class DruidDataSourceConfig {

    @Autowired
    private DataSourceProperties properties;

    @Bean
    public DataSource dataSoucre() throws Exception {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl(properties.getUrl());
        dataSource.setDriverClassName(properties.getDriverClassName());
        dataSource.setUsername(properties.getUsername());
        dataSource.setPassword(properties.getPassword());
        dataSource.setInitialSize(5);
        dataSource.setMinIdle(5);
        dataSource.setMaxActive(100);
        dataSource.setMaxWait(60000);
        dataSource.setTimeBetweenEvictionRunsMillis(60000);
        dataSource.setMinEvictableIdleTimeMillis(300000);
        dataSource.setValidationQuery("SELECT 'x'");
        dataSource.setTestWhileIdle(true);
        dataSource.setTestOnBorrow(false);
        dataSource.setTestOnReturn(false);
        dataSource.setPoolPreparedStatements(true);
        dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
        dataSource.setFilters("stat,wall");
        return dataSource;
    }
}

     

    使用 DruidDataSource  作為數據庫連接池可能看起來會比較麻煩,但是換一個角度來說,這個更加可控。我們可以通過  DataSourceProperties 來獲取 application.properties 中的配置文件:

spring.datasource.url=jdbc:mysql://localhost:3306/charles_blog2
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

    需要注意的是,DataSourceProperties 讀取的配置文件 前綴是 spring.datasource ,我們可以進入到 DataSourceProperties 的源碼中觀察:

@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties
        implements BeanClassLoaderAware, EnvironmentAware, InitializingBean

    可以看到,在源碼中已經默認標注了前綴的格式。

 

    除了使用 DataSourceProperties 來獲取配置文件 我們還可以使用通用的環境變量讀取類:

    @Autowired
    private Environment env;
  
  
    env.getProperty("spring.datasource.write")

 

  

 

2、多數據源配置

    配置多數據源主要需要以下幾個步驟:

    2.1 DatabaseType 數據源名稱

        這里直接使用枚舉類型區分,讀數據源和寫數據源

public enum DatabaseType {
    master("write"), slave("read");


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

    private String name;

    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "DatabaseType{" +
                "name='" + name + '\'' +
                '}';
    }
}

 

 

    2.2 DatabaseContextHolder 

      該類主要用於記錄當前線程使用的數據源,使用 ThreadLocal 進行記錄數據

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();
    }
}

 

    2.3 DynamicDataSource

     該類繼承 AbstractRoutingDataSource 用於管理 我們的數據源,主要實現了 determineCurrentLookupKey 方法。

     后續細述這個類是如何進行多數據源管理的。

public class DynamicDataSource extends AbstractRoutingDataSource {


    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        DatabaseType type = DatabaseContextHolder.getDatabaseType();
        logger.info("====================dataSource ==========" + type);
        return type;
    }

}

 

 

    2.4 DataSourceConfig

     最后一步就是配置我們的數據源,將數據源放置到 DynamicDataSource 中:

@Configuration
@MapperScan("com.jaycekon.demo.mapper")
@EnableTransactionManagement
public class DataSourceConfig {

    @Autowired
    private DataSourceProperties properties;

    /**
     * 通過Spring JDBC 快速創建 DataSource
     * 參數格式
     * spring.datasource.master.jdbcurl=jdbc:mysql://localhost:3306/charles_blog
     * spring.datasource.master.username=root
     * spring.datasource.master.password=root
     * spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver
     *
     * @return DataSource
     */
    @Bean(name = "masterDataSource")
    @Qualifier("masterDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

 /**
     * 手動創建DruidDataSource,通過DataSourceProperties 讀取配置
     * 參數格式
     * spring.datasource.url=jdbc:mysql://localhost:3306/charles_blog
     * spring.datasource.username=root
     * spring.datasource.password=root
     * spring.datasource.driver-class-name=com.mysql.jdbc.Driver
     *
     * @return DataSource
     * @throws SQLException
     */
    @Bean(name = "slaveDataSource")
    @Qualifier("slaveDataSource")
    public DataSource slaveDataSource() throws SQLException {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl(properties.getUrl());
        dataSource.setDriverClassName(properties.getDriverClassName());
        dataSource.setUsername(properties.getUsername());
        dataSource.setPassword(properties.getPassword());
        dataSource.setInitialSize(5);
        dataSource.setMinIdle(5);
        dataSource.setMaxActive(100);
        dataSource.setMaxWait(60000);
        dataSource.setTimeBetweenEvictionRunsMillis(60000);
        dataSource.setMinEvictableIdleTimeMillis(300000);
        dataSource.setValidationQuery("SELECT 'x'");
        dataSource.setTestWhileIdle(true);
        dataSource.setTestOnBorrow(false);
        dataSource.setTestOnReturn(false);
        dataSource.setPoolPreparedStatements(true);
        dataSource.setMaxPoolPreparedStatementPerConnectionSize(20);
        dataSource.setFilters("stat,wall");
        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;
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("masterDataSource") DataSource myTestDbDataSource,
                                               @Qualifier("slaveDataSource") DataSource myTestDb2DataSource) throws Exception {
        SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
        fb.setDataSource(this.dataSource(myTestDbDataSource, myTestDb2DataSource));
        fb.setTypeAliasesPackage(env.getProperty("mybatis.type-aliases-package"));
        fb.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(env.getProperty("mybatis.mapper-locations")));
        return fb.getObject();
    }
}

 

    上述代碼塊比較長,我們來解析一下:

  • masterDataSource 和 slaveDataSource 主要是用來創建數據源的,這里分別使用了 hikaridatasource 和  druidDataSource 作為數據源
  • DynamicDataSource 方法體中,我們主要是將兩個數據源都放到 DynamicDataSource 中進行統一管理
  • SqlSessionFactory  方法則是將所有數據源(DynamicDataSource )統一管理

 

    2.5 UserMapperTest

      接下來我們來簡單觀察一下 DataSource 的創建過程:

      首先我們可以看到我們的兩個數據源以及構建好了,分別使用的是HikariDataSource 和 DruidDataSource,然后我們會將兩個數據源放入到 targetDataSource 中,並且這里講我們的 slave 作為默認數據源 defaultTargetDataSource

      

    然后到獲取數據源這一塊:

    主要是從 AbstractRoutingDataSource 這個類中的 determineTargetDataSource( ) 方法中進行判斷,這里會調用到我們再 DynamicDataSource 中的方法, 去判斷需要使用哪一個數據源。如果沒有設置數據源,將采用默認數據源,就是我們剛才設置的DruidDataSource 數據源。

 

      在最后的代碼運行結果中:

      我們可以看到確實是使用了我們設置的默認數據源。

 

 

3、讀寫分離

      在經歷了千山萬水后,終於來到我們的讀寫分離模塊了,首先我們需要添加一些我們的配置信息:

spring.datasource.read = get,select,count,list,query
spring.datasource.write = add,create,update,delete,remove,insert

      這兩個變量主要用於切面判斷中,區分哪一些部分是需要使用 讀數據源,哪些是需要使用寫的。

 

    3.1 DynamicDataSource 修改

public class DynamicDataSource extends AbstractRoutingDataSource {

    static final Map<DatabaseType, List<String>> METHOD_TYPE_MAP = new HashMap<>();


    @Nullable
    @Override
    protected Object determineCurrentLookupKey() {
        DatabaseType type = DatabaseContextHolder.getDatabaseType();
        logger.info("====================dataSource ==========" + type);
        return type;
    }

    void setMethodType(DatabaseType type, String content) {
        List<String> list = Arrays.asList(content.split(","));
        METHOD_TYPE_MAP.put(type, list);
    }
}

    在這里我們需要添加一個Map 進行記錄一些讀寫的前綴信息。

 

 

    3.2 DataSourceConfig 修改

      在DataSourceConfig 中,我們再設置DynamicDataSource 的時候,將前綴信息設置進去。

@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設置為myTestDbDataSource

        String read = env.getProperty("spring.datasource.read");
        dataSource.setMethodType(DatabaseType.slave, read);

        String write = env.getProperty("spring.datasource.write");
        dataSource.setMethodType(DatabaseType.master, write);

        return dataSource;
    }

 

    3.3 DataSourceAspect

      在配置好讀寫的方法前綴后,我們需要配置一個切面,監聽在進入Mapper 方法前將數據源設置好:

      主要的操作點在於  DatabaseContextHolder.setDatabaseType(type); 結合我們上面多數據源的獲取數據源方法,這里就是我們設置讀或寫數據源的關鍵了。

@Aspect
@Component
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class DataSourceAspect {
    private static Logger logger = LoggerFactory.getLogger(DataSourceAspect.class);

    @Pointcut("execution(* com.jaycekon.demo.mapper.*.*(..))")
    public void aspect() {

    }


    @Before("aspect()")
    public void before(JoinPoint point) {
        String className = point.getTarget().getClass().getName();
        String method = point.getSignature().getName();
        String args = StringUtils.join(point.getArgs(), ",");
        logger.info("className:{}, method:{}, args:{} ", className, method, args);
        try {
            for (DatabaseType type : DatabaseType.values()) {
                List<String> values = DynamicDataSource.METHOD_TYPE_MAP.get(type);
                for (String key : values) {
                    if (method.startsWith(key)) {
                        logger.info(">>{} 方法使用的數據源為:{}<<", method, key);
                        DatabaseContextHolder.setDatabaseType(type);
                        DatabaseType types = DatabaseContextHolder.getDatabaseType();
                        logger.info(">>{}方法使用的數據源為:{}<<", method, types);
                    }
                }
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
    }
}

    

   

    3.4 UserMapperTest

      方法啟動后,先進入切面中,根據methodName 設置數據源類型。

 

      然后進入到determineTargetDataSource 方法中 獲取到數據源:

        運行結果:

 

 

 

 

4、寫在最后

  希望看完后覺得有幫助的朋友,幫博主到github 上面點個Start 或者 fork

  Spring-Blog 項目GitHub地址:https://github.com/jaycekon/Spring-Blog

  示例代碼 Github 地址:https://github.com/jaycekon/SpringBoot

 

  我的博客即將搬運同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan


免責聲明!

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



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