Spring-Mybatis 讀寫分離

2020-10-11 java架構網際網路攻城獅


概述:

技術棧方面,會採用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.0 的 Web模塊)

  為了讓各位朋友能夠更好理解這一模塊的內容,演示代碼將存放在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_blog2spring.datasource.username=rootspring.datasource.password=rootspring.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.MyMappermapper.not-empty=falsemapper.identity=MYSQL複製代碼

1.1 DataSourceBuilder

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

@Configuration@MapperScan("com.jaycekon.demo.mapper")@EnableTransactionManagementpublic 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@EnableTransactionManagementpublic 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_blog2spring.datasource.username=rootspring.datasource.password=rootspring.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")@EnableTransactionManagementpublic 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(); }}複製代碼

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

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

2.5 UserMapperTest

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

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

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

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

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

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

3、讀寫分離

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

spring.datasource.read = get,select,count,list,queryspring.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 方法中 獲取到數據源:

運行結果:

相關焦點

  • springboot+mybatis實現資料庫的讀寫分離
    介紹隨著業務的發展,除了拆分業務模塊外,資料庫的讀寫分離也是常見的優化手段。方案使用了AbstractRoutingDataSource和mybatis plugin來動態的選擇數據源選擇這個方案的原因主要是不需要改動原有業務代碼,非常友好註:demo中使用了mybatis-plus,實際使用mybatis也是一樣的demo中使用的資料庫是
  • SpringBoot+MyBatis+MySQL讀寫分離
    引言讀寫分離要做的事情就是對於一條SQL該選擇哪個資料庫去執行,至於誰來做選擇資料庫這件事兒,無非兩個,要麼中間件幫我們做,要麼程序自己做。因此,一般來講,讀寫分離有兩種實現方式。第一種是依靠中間件(比如:MyCat),也就是說應用程式連接到中間件,中間件幫我們做SQL分離;第二種是應用程式自己去做分離。
  • SpringBoot + MyBatis + MySQL讀寫分離實踐!
    引言讀寫分離要做的事情就是對於一條SQL該選擇哪個資料庫去執行,至於誰來做選擇資料庫這件事兒,無非兩個,要麼中間件幫我們做,要麼程序自己做。因此,一般來講,讀寫分離有兩種實現方式。第一種是依靠中間件(比如:MyCat),也就是說應用程式連接到中間件,中間件幫我們做SQL分離;第二種是應用程式自己去做分離。
  • 為什麼要使用讀寫分離?SpirngBoot+MyBatis如何實現讀寫分離?
    為什麼要使用讀寫分離?讀寫分離是網際網路公司常用的技術方案,用來分攤資料庫壓力的。我們知道在一般的網際網路項目中都是讀多寫少,這就導致資料庫在大部分時候都在進行讀操作,那麼在高並發、高流量讀的情況下,資料庫的讀操作就可能成為系統的性能瓶頸。
  • 實測有用,簡單幾步配置spring boot+mybatis-plus多數據源
    1 讀寫分離及多數據源在我們開發的大多數業務中,往往讀多寫少,資料庫的讀會首先變成資料庫使用的瓶頸。此時,我們必須能夠提升資料庫的讀性能。一般寫1萬條數據的時間比讀1萬條的數據時間大很多。讀寫鎖衝突從會限制資料庫讀的性能,讀寫分離來保證數據使用性能。用一句話概括,讀寫分離可以很好地解決資料庫的讀性能瓶頸的。
  • 學會MySQL主從複製讀寫分離,看這篇就夠了
    為什麼要採用主從複製讀寫分離的架構?如何實現?有什麼缺點?讓我們帶著這些問題開始這段學習之旅吧!為什麼使用主從複製、讀寫分離主從複製、讀寫分離一般是一起使用的。目的很簡單,就是為了提高資料庫的並發性能。
  • 一文讀懂 MySQL 主從複製讀寫分離
    為什麼要採用主從複製讀寫分離的架構?如何實現?有什麼缺點?讓我們帶著這些問題開始這段學習之旅吧!為什麼使用主從複製、讀寫分離主從複製、讀寫分離一般是一起使用的。目的很簡單,就是為了提高資料庫的並發性能。
  • 什麼是MySQL主從複製讀寫分離,看這篇就夠了
    為什麼要採用主從複製讀寫分離的架構?如何實現?有什麼缺點?讓我們帶著這些問題開始這段學習之旅吧!為什麼使用主從複製、讀寫分離主從複製、讀寫分離一般是一起使用的。目的很簡單,就是為了提高資料庫的並發性能。
  • ...發布,支持多資料庫、多數據源、讀寫分離的極速輕量級 Java ORM...
    SmartDb 是以SQL為中心,支持多數據源、讀寫分離,同時又不與原有項目mybatis、hibernate衝突的非常輕量級的Java ORM框架。
  • SmartDb 1.0.0 發布,支持多資料庫、多數據源、讀寫分離的極速...
    SmartDb 是以SQL為中心,支持多數據源、讀寫分離,同時又不與原有項目mybatis、hibernate衝突的非常輕量級的Java ORM框架。
  • ...1.0.0 發布,支持多資料庫、多數據源、讀寫分離的極速輕量級...
    SmartDb 是以SQL為中心,支持多數據源、讀寫分離,同時又不與原有項目mybatis、hibernate
  • Spring-data-mybatis案例
    本節主要學習Spring-data-mybatis案例,即spring集成mybatis框架。--mybatis-spring--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.5<
  • 最新Spring整合MyBatis詳解教程
    <dependency>    <groupId>org.mybatis</groupId>    <artifactId>mybatis-spring</artifactId>    <version>2.0.5</version></dependency>
  • Spring boot整合持久層框架Mybatis
    boot和mybatis的依賴,可以根據實際情況進行修改版本spring boot相關依賴<parent> <groupId>org.springframework.boot
  • Spring整合Mybatis框架基礎配置
    文件,因為和spring框架整合之後,我們連接資料庫的配置寫到了spring的配置文件中,所有mybatis的配置文件可以不寫了,但是,我們也可以寫一些配置。那我們這裡保留別名寫到mybatis的配置文件中。
  • SpringBoot學習筆記二之Spring整合Mybatis
    useUnicode=true&characterEncoding=UTF-8jdbc.driver=com.mysql.jdbc.Driver在learn-admin-webui中配置mybatis-config.xml(注意是在mybatis目錄中)配置內容
  • springboot mybatis jpa 實現讀寫分離
    環境:springboot2.2.11.RELEASE+MySQL+JPA+MyBatis這裡不使用第三方的,比如:mycat,sharedsphere。自己實現一個比較簡單的方案。實現方案:利用AOP攔截業務方法,比如:find,select,query等這類的方法通過從(讀)庫獲取數據,其它的都從主(write)庫操作。實戰!
  • MyBatis初級實戰之一:Spring Boot集成
    JDK:1.8.0_1812. mybatis-spring-boot-starter:2.1.3(對應的MyBatis:3.5.5)3. Spring Boot:2.3.2.RELEASE4. MySQL:5.7.295. 實戰環境:win106.
  • Spring boot集成mybatis以及使用的步驟
    mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version
  • Spring集成MyBatis | Spring系列第53篇
    2、案例代碼git地址:https://gitee.com/javacode2018/spring-series方式1源碼:spring-series\lesson-005-mybatis方式2源碼:spring-series\lesson-006-mybatis3、準備