spring boot + mybatis如何實(shí)現(xiàn)數(shù)據(jù)庫(kù)的讀寫分離
介紹
隨著業(yè)務(wù)的發(fā)展,除了拆分業(yè)務(wù)模塊外,數(shù)據(jù)庫(kù)的讀寫分離也是常見的優(yōu)化手段。
方案使用了AbstractRoutingDataSource和mybatis plugin來(lái)動(dòng)態(tài)的選擇數(shù)據(jù)源
選擇這個(gè)方案的原因主要是不需要改動(dòng)原有業(yè)務(wù)代碼,非常友好
注:
demo中使用了mybatis-plus,實(shí)際使用mybatis也是一樣的
demo中使用的數(shù)據(jù)庫(kù)是postgres,實(shí)際任一類型主從備份的數(shù)據(jù)庫(kù)示例都是一樣的
demo中使用了alibaba的druid數(shù)據(jù)源,實(shí)際其他類型的數(shù)據(jù)源也是一樣的
環(huán)境
首先,我們需要兩個(gè)數(shù)據(jù)庫(kù)實(shí)例,一為master,一為slave。
所有的寫操作,我們?cè)趍aster節(jié)點(diǎn)上操作
所有的讀操作,我們?cè)趕lave節(jié)點(diǎn)上操作
需要注意的是:對(duì)于一次有讀有寫的事務(wù),事務(wù)內(nèi)的讀操作也不應(yīng)該在slave節(jié)點(diǎn)上,所有操作都應(yīng)該在master節(jié)點(diǎn)上
先跑起來(lái)兩個(gè)pg的實(shí)例,其中15432端口對(duì)應(yīng)的master節(jié)點(diǎn),15433端口對(duì)應(yīng)的slave節(jié)點(diǎn):
docker run \ --name pg-master \ -p 15432:5432 \ --env 'PG_PASSWORD=postgres' \ --env 'REPLICATION_MODE=master' \ --env 'REPLICATION_USER=repluser' \ --env 'REPLICATION_PASS=repluserpass' \ -d sameersbn/postgresql:10-2 docker run \ --name pg-slave \ -p 15433:5432 \ --link pg-master:master \ --env 'PG_PASSWORD=postgres' \ --env 'REPLICATION_MODE=slave' \ --env 'REPLICATION_SSLMODE=prefer' \ --env 'REPLICATION_HOST=master' \ --env 'REPLICATION_PORT=5432' \ --env 'REPLICATION_USER=repluser' \ --env 'REPLICATION_PASS=repluserpass' \ -d sameersbn/postgresql:10-2
實(shí)現(xiàn)
整個(gè)實(shí)現(xiàn)主要有3個(gè)部分:
- 配置兩個(gè)數(shù)據(jù)源
- 實(shí)現(xiàn)AbstractRoutingDataSource來(lái)動(dòng)態(tài)的使用數(shù)據(jù)源
- 實(shí)現(xiàn)mybatis plugin來(lái)動(dòng)態(tài)的選擇數(shù)據(jù)源
配置數(shù)據(jù)源
將數(shù)據(jù)庫(kù)連接信息配置到application.yml文件中
spring:
mvc:
servlet:
path: /api
datasource:
write:
driver-class-name: org.postgresql.Driver
url: "${DB_URL_WRITE:jdbc:postgresql://localhost:15432/postgres}"
username: "${DB_USERNAME_WRITE:postgres}"
password: "${DB_PASSWORD_WRITE:postgres}"
read:
driver-class-name: org.postgresql.Driver
url: "${DB_URL_READ:jdbc:postgresql://localhost:15433/postgres}"
username: "${DB_USERNAME_READ:postgres}"
password: "${DB_PASSWORD_READ:postgres}"
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
write寫數(shù)據(jù)源,對(duì)應(yīng)到master節(jié)點(diǎn)的15432端口
read讀數(shù)據(jù)源,對(duì)應(yīng)到slave節(jié)點(diǎn)的15433端口
將兩個(gè)數(shù)據(jù)源信息注入為DataSourceProperties:
@Configuration
public class DataSourcePropertiesConfig {
@Primary
@Bean("writeDataSourceProperties")
@ConfigurationProperties("datasource.write")
public DataSourceProperties writeDataSourceProperties() {
return new DataSourceProperties();
}
@Bean("readDataSourceProperties")
@ConfigurationProperties("datasource.read")
public DataSourceProperties readDataSourceProperties() {
return new DataSourceProperties();
}
}
實(shí)現(xiàn)AbstractRoutingDataSource
spring提供了AbstractRoutingDataSource,提供了動(dòng)態(tài)選擇數(shù)據(jù)源的功能,替換原有的單一數(shù)據(jù)源后,即可實(shí)現(xiàn)讀寫分離:
@Component
public class CustomRoutingDataSource extends AbstractRoutingDataSource {
@Resource(name = "writeDataSourceProperties")
private DataSourceProperties writeProperties;
@Resource(name = "readDataSourceProperties")
private DataSourceProperties readProperties;
@Override
public void afterPropertiesSet() {
DataSource writeDataSource =
writeProperties.initializeDataSourceBuilder().type(DruidDataSource.class).build();
DataSource readDataSource =
readProperties.initializeDataSourceBuilder().type(DruidDataSource.class).build();
setDefaultTargetDataSource(writeDataSource);
Map<Object, Object> dataSourceMap = new HashMap<>();
dataSourceMap.put(WRITE_DATASOURCE, writeDataSource);
dataSourceMap.put(READ_DATASOURCE, readDataSource);
setTargetDataSources(dataSourceMap);
super.afterPropertiesSet();
}
@Override
protected Object determineCurrentLookupKey() {
String key = DataSourceHolder.getDataSource();
if (key == null) {
// default datasource
return WRITE_DATASOURCE;
}
return key;
}
}
AbstractRoutingDataSource內(nèi)部維護(hù)了一個(gè)Map<Object, Object>的Map
在初始化過(guò)程中,我們將write、read兩個(gè)數(shù)據(jù)源加入到這個(gè)map
調(diào)用數(shù)據(jù)源時(shí):determineCurrentLookupKey()方法返回了需要使用的數(shù)據(jù)源對(duì)應(yīng)的key
當(dāng)前線程需要使用的數(shù)據(jù)源對(duì)應(yīng)的key,是在DataSourceHolder類中維護(hù)的:
public class DataSourceHolder {
public static final String WRITE_DATASOURCE = "write";
public static final String READ_DATASOURCE = "read";
private static final ThreadLocal<String> local = new ThreadLocal<>();
public static void putDataSource(String dataSource) {
local.set(dataSource);
}
public static String getDataSource() {
return local.get();
}
public static void clearDataSource() {
local.remove();
}
}
實(shí)現(xiàn)mybatis plugin
上面提到了當(dāng)前線程使用的數(shù)據(jù)源對(duì)應(yīng)的key,這個(gè)key需要在mybatis plugin根據(jù)sql類型來(lái)確定
MybatisDataSourceInterceptor類:
@Component
@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}),
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class,
CacheKey.class, BoundSql.class})})
public class MybatisDataSourceInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
boolean synchronizationActive = TransactionSynchronizationManager.isSynchronizationActive();
if(!synchronizationActive) {
Object[] objects = invocation.getArgs();
MappedStatement ms = (MappedStatement) objects[0];
if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
DataSourceHolder.putDataSource(DataSourceHolder.READ_DATASOURCE);
}
}
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
僅當(dāng)未在事務(wù)中,并且調(diào)用的sql是select類型時(shí),在DataSourceHolder中將數(shù)據(jù)源設(shè)為read
其他情況下,AbstractRoutingDataSource會(huì)使用默認(rèn)的write數(shù)據(jù)源
至此,項(xiàng)目已經(jīng)可以自動(dòng)的在讀、寫數(shù)據(jù)源間切換,無(wú)需修改原有的業(yè)務(wù)代碼
最后,提供demo使用依賴版本
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatisplus-spring-boot-starter</artifactId>
<version>1.0.5</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>2.1.9</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.8.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
Java Map遍歷2種實(shí)現(xiàn)方法代碼實(shí)例
這篇文章主要介紹了Java Map遍歷2種實(shí)現(xiàn)方法代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-10-10
OpenFeign超時(shí)時(shí)間設(shè)置不生效問(wèn)題排查記錄
文章主要講述了在升級(jí)Spring Boot 3后,發(fā)現(xiàn)配置文件中的OpenFeign超時(shí)時(shí)間設(shè)置不生效的問(wèn)題,通過(guò)查看FeignClientFactoryBean類和FeignClientProperties類的源碼,發(fā)現(xiàn)配置讀取的方式發(fā)生了變化,從而導(dǎo)致超時(shí)時(shí)間設(shè)置不生效2024-11-11
itextpdf提取PDF文件中的任意頁(yè)碼實(shí)現(xiàn)示例
這篇文章主要為大家介紹了itextpdf提取PDF文件中的任意頁(yè)碼實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
SpringBoot+layui實(shí)現(xiàn)文件上傳功能
Spring Boot是由Pivotal團(tuán)隊(duì)提供的全新框架,其設(shè)計(jì)目的是用來(lái)簡(jiǎn)化新Spring應(yīng)用的初始搭建以及開發(fā)過(guò)程。這篇文章主要介紹了SpringBoot+layui實(shí)現(xiàn)文件上傳,需要的朋友可以參考下2018-09-09
一文詳解SpringBoot響應(yīng)壓縮功能的配置與優(yōu)化
Spring Boot的響應(yīng)壓縮功能基于智能協(xié)商機(jī)制,需同時(shí)滿足很多條件,本文主要為大家詳細(xì)介紹了SpringBoot響應(yīng)壓縮功能的配置與優(yōu)化,需要的可以參考下2025-03-03
Java字符串?dāng)?shù)組的創(chuàng)建代碼示例
這篇文章主要介紹了Java中字符串?dāng)?shù)組的聲明、初始化、默認(rèn)值、遍歷和常見操作,文中通過(guò)代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2025-03-03
@Transactional注解異常報(bào)錯(cuò)之多數(shù)據(jù)源詳解
這篇文章主要介紹了@Transactional注解異常報(bào)錯(cuò)之多數(shù)據(jù)源詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01
SpringBoot與knife4j的整合使用過(guò)程
Knife4j?是一個(gè)基于Swagger構(gòu)建的開源?JavaAPI文檔工具,主要包括兩大核心功能:文檔說(shuō)明和在線調(diào)試,這篇文章主要介紹了SpringBoot與knife4j的整合使用,需要的朋友可以參考下2024-08-08

