Spring-Mybatis 讀寫分離

JayceKon發表於2018-01-19

概述:

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

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

  專案GitHub地址: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 地址:示例程式碼    

1、DataSource

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

Spring-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 建立以及關閉的過程。

Spring-Mybatis 讀寫分離

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   

Spring-Mybatis 讀寫分離

然後到獲取資料來源這一塊:

    主要是從 AbstractRoutingDataSource 這個類中的 determineTargetDataSource( ) 方法中進行判斷,這裡會呼叫到我們在 DynamicDataSource 中的方法, 去判斷需要使用哪一個資料來源。如果沒有設定資料來源,將採用預設資料來源,就是我們剛才設定的DruidDataSource 資料來源。    

Spring-Mybatis 讀寫分離

      在最後的程式碼執行結果中:

      我們可以看到確實是使用了我們設定的預設資料來源。      

Spring-Mybatis 讀寫分離

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 設定資料來源型別。 

Spring-Mybatis 讀寫分離

然後進入到determineTargetDataSource 方法中 獲取到資料來源:

Spring-Mybatis 讀寫分離

執行結果:

Spring-Mybatis 讀寫分離

4、寫在最後

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

  Spring-Blog 專案GitHub地址:Spring-Blog

  示例程式碼 Github 地址:示例程式碼

來源:https://juejin.im/post/5a61a0475188257324724345

相關文章