SpringBoot配置主從數(shù)據(jù)庫(kù)實(shí)現(xiàn)讀寫(xiě)分離
一、前言
現(xiàn)在的 Web 應(yīng)用大都是讀多寫(xiě)少。除了緩存以外還可以通過(guò)數(shù)據(jù)庫(kù) “主從復(fù)制” 架構(gòu),把讀請(qǐng)求路由到從數(shù)據(jù)庫(kù)節(jié)點(diǎn)上,實(shí)現(xiàn)讀寫(xiě)分離,從而大大提高應(yīng)用的吞吐量。
通常,我們?cè)?Spring Boot 中只會(huì)用到一個(gè)數(shù)據(jù)源,即通過(guò) spring.datasource 進(jìn)行配置。前文 《在 Spring Boot 中配置和使用多個(gè)數(shù)據(jù)源》 介紹了一種在 Spring Boot 中定義、使用多個(gè)數(shù)據(jù)源的方式。但是這種方式對(duì)于實(shí)現(xiàn) “讀寫(xiě)分離” 的場(chǎng)景不太適合。首先,多個(gè)數(shù)據(jù)源都是通過(guò) @Bean 定義的,當(dāng)需要新增額外的從數(shù)據(jù)庫(kù)時(shí)需要改動(dòng)代碼,非常不夠靈活。其次,在業(yè)務(wù)層中,如果需要根據(jù)讀、寫(xiě)場(chǎng)景切換不同數(shù)據(jù)源的話只能手動(dòng)進(jìn)行。
對(duì)于 Spring Boot “讀寫(xiě)分離” 架構(gòu)下的的多數(shù)據(jù)源,我們需要實(shí)現(xiàn)如下需求:
- 可以通過(guò)配置文件新增數(shù)據(jù)庫(kù)(從庫(kù)),而不不需要修改代碼。
- 自動(dòng)根據(jù)場(chǎng)景切換讀、寫(xiě)數(shù)據(jù)源,對(duì)業(yè)務(wù)層是透明的。
幸運(yùn)的是,Spring Jdbc 模塊類(lèi)提供了一個(gè) AbstractRoutingDataSource 抽象類(lèi)可以實(shí)現(xiàn)我們的需求。
它本身也實(shí)現(xiàn)了 DataSource 接口,表示一個(gè) “可路由” 的數(shù)據(jù)源。
核心的代碼如下:
public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean {
// 維護(hù)的所有數(shù)據(jù)源
@Nullable
private Map<Object, DataSource> resolvedDataSources;
// 默認(rèn)的數(shù)據(jù)源
@Nullable
private DataSource resolvedDefaultDataSource;
// 獲取 Jdbc 連接
@Override
public Connection getConnection() throws SQLException {
return determineTargetDataSource().getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return determineTargetDataSource().getConnection(username, password);
}
// 獲取目標(biāo)數(shù)據(jù)源
protected DataSource determineTargetDataSource() {
Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");
// 調(diào)用 determineCurrentLookupKey() 抽象方法,獲取 resolvedDataSources 中定義的 key。
Object lookupKey = determineCurrentLookupKey();
DataSource dataSource = this.resolvedDataSources.get(lookupKey);
if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
dataSource = this.resolvedDefaultDataSource;
}
if (dataSource == null) {
throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
}
return dataSource;
}
// 抽象方法,返回 resolvedDataSources 中定義的 key。需要自己實(shí)現(xiàn)
@Nullable
protected abstract Object determineCurrentLookupKey();
}
核心代碼如上,它的工作原理一目了然。它在內(nèi)部維護(hù)了一個(gè) Map<Object, DataSource> 屬性,維護(hù)了多個(gè)數(shù)據(jù)源。
當(dāng)嘗試從 AbstractRoutingDataSource 數(shù)據(jù)源獲取數(shù)據(jù)源連接對(duì)象 Connection 時(shí),會(huì)調(diào)用 determineCurrentLookupKey() 方法得到一個(gè) Key,然后從數(shù)據(jù)源 Map<Object, DataSource> 中獲取到真正的目標(biāo)數(shù)據(jù)源,如果 Key 或者是目標(biāo)數(shù)據(jù)源為 null 則使用默認(rèn)的數(shù)據(jù)源。
得到目標(biāo)數(shù)據(jù)數(shù)據(jù)源后,返回真正的 Jdbc 連接。這一切對(duì)于使用到 Jdbc 的組件(Repository、JdbcTemplate 等)來(lái)說(shuō)都是透明的。
了解了 AbstractRoutingDataSource 后,我們來(lái)看看如何使用它來(lái)實(shí)現(xiàn) “讀寫(xiě)分離”。
二、實(shí)現(xiàn)思路
首先,創(chuàng)建自己的 AbstractRoutingDataSource 實(shí)現(xiàn)類(lèi)。把它的默認(rèn)數(shù)據(jù)源 resolvedDefaultDataSource 設(shè)置為主庫(kù),從庫(kù)則保存到 Map<Object, DataSource> resolvedDataSources 中。
在 Spring Boot 應(yīng)用中通常使用 @Transactional 注解來(lái)開(kāi)啟聲明式事務(wù),它的默認(rèn)傳播級(jí)別為 REQUIRED,也就是保證多個(gè)事務(wù)方法之間的相互調(diào)用都是在同一個(gè)事務(wù)中,使用的是同一個(gè) Jdbc 連接。它還有一個(gè) readOnly 屬性表示是否是只讀事務(wù)。
于是,我們可以通過(guò) AOP 技術(shù),在事務(wù)方法執(zhí)行之前,先獲取到方法上的 @Transactional 注解從而判斷是讀、還是寫(xiě)業(yè)務(wù)。并且把 “讀寫(xiě)狀態(tài)” 存儲(chǔ)到線程上下文(ThreadLocal)中!
在 AbstractRoutingDataSource 的 determineCurrentLookupKey 方法中,我們就可以根據(jù)當(dāng)前線程上下文中的 “讀寫(xiě)狀態(tài)” 判斷當(dāng)前是否是只讀業(yè)務(wù),如果是,則返回從庫(kù) resolvedDataSources 中的 Key,反之則返回 null 表示使用默認(rèn)數(shù)據(jù)源也就是主庫(kù)。
三、初始化數(shù)據(jù)庫(kù)
首先,在本地創(chuàng)建 4 個(gè)不同名稱(chēng)的數(shù)據(jù)庫(kù),用于模擬 “MYSQL 主從” 架構(gòu)。
-- 主庫(kù) CREATE DATABASE `demo_master` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci'; -- 從庫(kù) CREATE DATABASE `demo_slave1` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci'; -- 從庫(kù) CREATE DATABASE `demo_slave2` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci'; -- 從庫(kù) CREATE DATABASE `demo_slave3` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci';
如上,創(chuàng)建了 4 個(gè)數(shù)據(jù)庫(kù)。1 個(gè)主庫(kù),3 個(gè)從庫(kù)。它們本質(zhì)上毫無(wú)關(guān)系,并不是真正意義上的主從架構(gòu),這里只是為了方便演示。
接著,在這 4 個(gè)數(shù)據(jù)庫(kù)下依次執(zhí)行如下 SQL 創(chuàng)建一張名為 test 的表。
該表只有 2 個(gè)字段,1 個(gè)是 id 表示主鍵,一個(gè)是 name 表示名稱(chēng)。
CREATE TABLE `test` ( `id` int NOT NULL COMMENT 'ID', `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '名稱(chēng)', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
最后,初始化數(shù)據(jù)。往不同的數(shù)據(jù)庫(kù)插入對(duì)應(yīng)的記錄。
INSERT INTO `demo_master`.`test` (`id`, `name`) VALUES (1, 'master'); INSERT INTO `demo_slave1`.`test` (`id`, `name`) VALUES (1, 'slave1'); INSERT INTO `demo_slave2`.`test` (`id`, `name`) VALUES (1, 'slave2'); INSERT INTO `demo_slave3`.`test` (`id`, `name`) VALUES (1, 'slave3');
不同數(shù)據(jù)庫(kù)節(jié)點(diǎn)下 test 表中的 name 字段不同,用于區(qū)別不同的數(shù)據(jù)庫(kù)節(jié)點(diǎn)。
四、創(chuàng)建應(yīng)用
創(chuàng)建 Spring Boot 應(yīng)用,添加 spring-boot-starter-jdbc 和 mysql-connector-j (MYSQL 驅(qū)動(dòng))依賴(lài):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
五、配置定義
我們需要在 application.yaml 中定義上面創(chuàng)建好的所有主、從數(shù)據(jù)庫(kù)。
app:
datasource:
master: # 唯一主庫(kù)
jdbcUrl: jdbc:mysql://127.0.0.1:3306/demo_master?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8&allowMultiQueries=true
username: root
password: root
slave: # 多個(gè)從庫(kù)
slave1:
jdbcUrl: jdbc:mysql://127.0.0.1:3306/demo_slave1?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8&allowMultiQueries=true
username: root
password: root
slave2:
jdbcUrl: jdbc:mysql://127.0.0.1:3306/demo_slave2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8&allowMultiQueries=true
username: root
password: root
slave3:
jdbcUrl: jdbc:mysql://127.0.0.1:3306/demo_slave3?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2b8&allowMultiQueries=true
username: root
password: root
在 app.datasource.master 下配置了唯一的一個(gè)主庫(kù),也就是寫(xiě)庫(kù)。然后在 app.datasource.slave 下以 Map 形式配置了多個(gè)從庫(kù)(也就是讀庫(kù)),每個(gè)從庫(kù)使用自定義的名稱(chēng)作為 Key。
數(shù)據(jù)源的實(shí)現(xiàn)使用的是默認(rèn)的 HikariDataSource,并且數(shù)據(jù)源的配置是按照 HikariConfig 類(lèi)定義的。也就是說(shuō),你可以根據(jù) HikariConfig 的屬性在配置中添加額外的設(shè)置。
有了配置后,還需要定義對(duì)應(yīng)的配置類(lèi),如下:
package cn.springdoc.demo.db;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.ConstructorBinding;
@ConfigurationProperties(prefix = "app.datasource") // 配置前綴
public class MasterSlaveDataSourceProperties {
// 主庫(kù)
private final Properties master;
// 從庫(kù)
private final Map<String, Properties> slave;
@ConstructorBinding // 通過(guò)構(gòu)造函數(shù)注入配置文件中的值
public MasterSlaveDataSourceProperties(Properties master, Map<String, Properties> slave) {
super();
Objects.requireNonNull(master);
Objects.requireNonNull(slave);
this.master = master;
this.slave = slave;
}
public Properties master() {
return master;
}
public Map<String, Properties> slave() {
return slave;
}
}
還需要在 main 類(lèi)上使用 @EnableConfigurationProperties 注解來(lái)加載我們的配置類(lèi):
package cn.springdoc.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import cn.springdoc.demo.db.MasterSlaveDataSourceProperties;
@SpringBootApplication
@EnableAspectJAutoProxy
@EnableConfigurationProperties(value = {MasterSlaveDataSourceProperties.class}) // 指定要加載的配置類(lèi)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
這里還使用 @EnableAspectJAutoProxy 開(kāi)啟了 AOP 的支持,后面會(huì)用到。
六、創(chuàng)建 MasterSlaveDataSourceMarker
創(chuàng)建一個(gè) MasterSlaveDataSourceMarker 類(lèi),用于維護(hù)當(dāng)前業(yè)務(wù)的 “讀寫(xiě)狀態(tài)”。
package cn.springdoc.demo.db;
public class MasterSlaveDataSourceMarker {
private static final ThreadLocal<Boolean> flag = new ThreadLocal<Boolean>();
// 返回標(biāo)記
public static Boolean get() {
return flag.get();
}
// 寫(xiě)狀態(tài),標(biāo)記為主庫(kù)
public static void master() {
flag.set(Boolean.TRUE);
}
// 讀狀態(tài),標(biāo)記為從庫(kù)
public static void slave() {
flag.set(Boolean.FALSE);
}
// 清空標(biāo)記
public static void clean() {
flag.remove();
}
}
通過(guò) ThreadLocal<Boolean> 在當(dāng)前線程中保存當(dāng)前業(yè)務(wù)的讀寫(xiě)狀態(tài)。
如果 get() 返回 null 或者 true 則表示非只讀,需要使用主庫(kù)。反之則表示只讀業(yè)務(wù),使用從庫(kù)。
七、創(chuàng)建 MasterSlaveDataSourceAop
創(chuàng)建 MasterSlaveDataSourceAop 切面類(lèi),在事務(wù)方法開(kāi)始之前執(zhí)行。
package cn.springdoc.demo.db;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE) // 在事務(wù)開(kāi)始之前執(zhí)行
public class MasterSlaveDataSourceAop {
static final Logger log = LoggerFactory.getLogger(MasterSlaveDataSourceAop.class);
@Pointcut(value = "@annotation(org.springframework.transaction.annotation.Transactional)")
public void txMethod () {}
@Around("txMethod()")
public Object handle (ProceedingJoinPoint joinPoint) throws Throwable {
// 獲取當(dāng)前請(qǐng)求的主從標(biāo)識(shí)
try {
// 獲取事務(wù)方法上的注解
Transactional transactional = ((MethodSignature) joinPoint.getSignature()).getMethod().getAnnotation(Transactional.class);
if (transactional != null && transactional.readOnly()) {
log.info("標(biāo)記為從庫(kù)");
MasterSlaveDataSourceMarker.slave(); // 只讀,從庫(kù)
} else {
log.info("標(biāo)記為主庫(kù)");
MasterSlaveDataSourceMarker.master(); // 可寫(xiě),主庫(kù)
}
// 執(zhí)行業(yè)務(wù)方法
Object ret = joinPoint.proceed();
return ret;
} catch (Throwable e) {
throw e;
} finally {
MasterSlaveDataSourceMarker.clean();
}
}
}
首先,通過(guò) @Order(Ordered.HIGHEST_PRECEDENCE) 注解保證它必須比聲明式事務(wù) AOP 更先執(zhí)行。
該 AOP 會(huì)攔截所有聲明了 @Transactional 的方法,在執(zhí)行前從該注解獲取 readOnly 屬性從而判斷是否是只讀業(yè)務(wù),并且在 MasterSlaveDataSourceMarker 標(biāo)記。
八、創(chuàng)建 MasterSlaveDataSource
現(xiàn)在,創(chuàng)建 AbstractRoutingDataSource 的實(shí)現(xiàn)類(lèi) MasterSlaveDataSource:
package cn.springdoc.demo.db;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
public class MasterSlaveDataSource extends AbstractRoutingDataSource {
static final Logger log = LoggerFactory.getLogger(MasterSlaveDataSource.class);
// 從庫(kù)的 Key 列表
private List<Object> slaveKeys;
// 從庫(kù) key 列表的索引
private AtomicInteger index = new AtomicInteger(0);
@Override
protected Object determineCurrentLookupKey() {
// 當(dāng)前線程的主從標(biāo)識(shí)
Boolean master = MasterSlaveDataSourceMarker.get();
if (master == null || master || this.slaveKeys.isEmpty()) {
// 主庫(kù),返回 null,使用默認(rèn)數(shù)據(jù)源
log.info("數(shù)據(jù)庫(kù)路由:主庫(kù)");
return null;
}
// 從庫(kù),從 slaveKeys 中選擇一個(gè) Key
int index = this.index.getAndIncrement() % this.slaveKeys.size();
if (this.index.get() > 9999999) {
this.index.set(0);
}
Object key = slaveKeys.get(index);
log.info("數(shù)據(jù)庫(kù)路由:從庫(kù) = {}", key);
return key;
}
public List<Object> getSlaveKeys() {
return slaveKeys;
}
public void setSlaveKeys(List<Object> slaveKeys) {
this.slaveKeys = slaveKeys;
}
}
其中,定義了一個(gè) List<Object> slaveKeys 字段,用于存儲(chǔ)在配置文件中定義的所有從庫(kù)的 Key。
在 determineCurrentLookupKey 方法中,判斷當(dāng)前業(yè)務(wù)的 “讀寫(xiě)狀態(tài)”,如果是只讀則通過(guò) AtomicInteger 原子類(lèi)自增后從 slaveKeys 輪詢(xún)出一個(gè)從庫(kù)的 Key。反之則返回 null 使用主庫(kù)。
九、創(chuàng)建 MasterSlaveDataSourceConfiguration 配置類(lèi)
最后,需要在 @Configuration 配置類(lèi)中,創(chuàng)建 MasterSlaveDataSource 數(shù)據(jù)源 Bean。
package cn.springdoc.demo.db;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.sql.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
@Configuration
public class MasterSlaveDataSourceConfiguration {
@Bean
public DataSource dataSource(MasterSlaveDataSourceProperties properties) {
MasterSlaveDataSource dataSource = new MasterSlaveDataSource();
// 主數(shù)據(jù)庫(kù)
dataSource.setDefaultTargetDataSource(new HikariDataSource(new HikariConfig(properties.master())));
// 從數(shù)據(jù)庫(kù)
Map<Object, Object> slaveDataSource = new HashMap<>();
// 從數(shù)據(jù)庫(kù) Key
dataSource.setSlaveKeys(new ArrayList<>());
for (Map.Entry<String,Properties> entry : properties.slave().entrySet()) {
if (slaveDataSource.containsKey(entry.getKey())) {
throw new IllegalArgumentException("存在同名的從數(shù)據(jù)庫(kù)定義:" + entry.getKey());
}
slaveDataSource.put(entry.getKey(), new HikariDataSource(new HikariConfig(entry.getValue())));
dataSource.getSlaveKeys().add(entry.getKey());
}
// 設(shè)置從庫(kù)
dataSource.setTargetDataSources(slaveDataSource);
return dataSource;
}
}
首先,通過(guò)配置方法注入配置類(lèi),該類(lèi)定義了配置文件中的主庫(kù)、從庫(kù)屬性。
使用 HikariDataSource 實(shí)例化唯一主庫(kù)數(shù)據(jù)源、和多個(gè)從庫(kù)數(shù)據(jù)源,并且設(shè)置到 MasterSlaveDataSource 對(duì)應(yīng)的屬性中。
同時(shí)還存儲(chǔ)每個(gè)從庫(kù)的 Key,且該 Key 不允許重復(fù)。
十、測(cè)試
1、創(chuàng)建 TestService
創(chuàng)建用于測(cè)試的業(yè)務(wù)類(lèi)。
package cn.springdoc.demo.service;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class TestService {
final JdbcTemplate jdbcTemplate;
public TestService(JdbcTemplate jdbcTemplate) {
super();
this.jdbcTemplate = jdbcTemplate;
}
// 只讀
@Transactional(readOnly = true)
public String read () {
return this.jdbcTemplate.queryForObject("SELECT `name` FROM `test` WHERE id = 1;", String.class);
}
// 先讀,再寫(xiě)
@Transactional
public String write () {
this.jdbcTemplate.update("UPDATE `test` SET `name` = ? WHERE id = 1;", "new name");
return this.read();
}
}
通過(guò)構(gòu)造函數(shù)注入 JdbcTemplate(spring jdbc 模塊自動(dòng)配置的)。
Service 類(lèi)中定義了 2 個(gè)方法。
- read():只讀業(yè)務(wù),從表中檢索 name 字段返回。
- write:可寫(xiě)業(yè)務(wù),先修改表中的 name 字段值為: new name,然后再調(diào)用 read() 方法讀取修改后的結(jié)果、返回。
2、創(chuàng)建測(cè)試類(lèi)
創(chuàng)建測(cè)試類(lèi),如下:
package cn.springdoc.demo.test;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import cn.springdoc.demo.service.TestService;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class DemoApplicationTests {
static final Logger log = LoggerFactory.getLogger(DemoApplicationTests.class);
@Autowired
TestService testService;
@Test
public void test() throws Exception {
// 連續(xù)4次讀
log.info("read={}", this.testService.read());
log.info("read={}", this.testService.read());
log.info("read={}", this.testService.read());
log.info("read={}", this.testService.read());
// 寫(xiě)
log.info("write={}", this.testService.write());
}
}
在測(cè)試類(lèi)方法中,連續(xù)調(diào)用 4 次 TestService 的 read() 方法。由于這是一個(gè)只讀方法,按照我們的設(shè)定,它會(huì)在 3 個(gè)從庫(kù)之間輪詢(xún)使用。由于我們故意把三個(gè)從庫(kù) test 表中 name 的字段值設(shè)置得不一樣,所以這里可以通過(guò)返回的結(jié)果看出來(lái)是否符合我們的預(yù)期。
最后調(diào)用了一次 write() 方法,按照設(shè)定會(huì)路由到主庫(kù)。先 UPDATE 修改數(shù)據(jù),再調(diào)用 read() 讀取數(shù)據(jù),雖然 read() 設(shè)置了 @Transactional(readOnly = true),但因?yàn)槿肟诜椒ㄊ?write(),所以 read() 還是會(huì)從主庫(kù)讀取數(shù)據(jù)(默認(rèn)的事務(wù)傳播級(jí)別)。
執(zhí)行測(cè)試,輸出的日志如下:
[ main] c.s.demo.db.MasterSlaveDataSourceAop : 標(biāo)記為從庫(kù)
[ main] c.s.demo.db.MasterSlaveDataSource : 數(shù)據(jù)庫(kù)路由:從庫(kù) = slave1
[ main] c.s.demo.test.DemoApplicationTests : read=slave1
[ main] c.s.demo.db.MasterSlaveDataSourceAop : 標(biāo)記為從庫(kù)
[ main] c.s.demo.db.MasterSlaveDataSource : 數(shù)據(jù)庫(kù)路由:從庫(kù) = slave2
[ main] c.s.demo.test.DemoApplicationTests : read=slave2
[ main] c.s.demo.db.MasterSlaveDataSourceAop : 標(biāo)記為從庫(kù)
[ main] c.s.demo.db.MasterSlaveDataSource : 數(shù)據(jù)庫(kù)路由:從庫(kù) = slave3
[ main] c.s.demo.test.DemoApplicationTests : read=slave3
[ main] c.s.demo.db.MasterSlaveDataSourceAop : 標(biāo)記為從庫(kù)
[ main] c.s.demo.db.MasterSlaveDataSource : 數(shù)據(jù)庫(kù)路由:從庫(kù) = slave1
[ main] c.s.demo.test.DemoApplicationTests : read=slave1
[ main] c.s.demo.db.MasterSlaveDataSourceAop : 標(biāo)記為主庫(kù)
[ main] c.s.demo.db.MasterSlaveDataSource : 數(shù)據(jù)庫(kù)路由:主庫(kù)
[ main] c.s.demo.test.DemoApplicationTests : write=new name
你可以看到,對(duì)于只讀業(yè)務(wù)。確實(shí)輪詢(xún)了三個(gè)不同的從庫(kù),符合預(yù)期。最后的 write() 方法也成功地路由到了主庫(kù),執(zhí)行了修改并且返回了修改后的結(jié)果。
十一總結(jié)
通過(guò) AbstractRoutingDataSource 可以不使用任何第三方中間件就可以在 Spring Boot 中實(shí)現(xiàn)數(shù)據(jù)源 “讀寫(xiě)分離”,這種方式需要在每個(gè)業(yè)務(wù)方法上通過(guò) @Transactional 注解明確定義是讀還是寫(xiě)。
到此這篇關(guān)于SpringBoot配置主從數(shù)據(jù)庫(kù)實(shí)現(xiàn)讀寫(xiě)分離的文章就介紹到這了,更多相關(guān)SpringBoot 讀寫(xiě)分離內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot實(shí)現(xiàn)單元測(cè)試示例詳解
單元測(cè)試(unit testing),是指對(duì)軟件中的最小可測(cè)試單元進(jìn)行檢查和驗(yàn)證。這篇文章主要為大家介紹了C語(yǔ)言實(shí)現(xiàn)單元測(cè)試的方法,需要的可以參考一下2022-11-11
Java的Synchronized關(guān)鍵字學(xué)習(xí)指南(全面 & 詳細(xì))
這篇文章主要給大家介紹了關(guān)于Java的Synchronized關(guān)鍵字的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03
Quarkus改造Pmml模型項(xiàng)目異常記錄及解決處理
這篇文章主要為大家介紹了Quarkus改造Pmml模型項(xiàng)目是遇到的異常記錄以及解決方法,有需要的同學(xué)可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-02-02
Java如何實(shí)現(xiàn)遠(yuǎn)程文件下載到本地目錄
本文介紹了如何使用Java來(lái)實(shí)現(xiàn)遠(yuǎn)程文件的下載功能,主要通過(guò)HTTPS路徑下載文件到本地目錄,詳細(xì)介紹了相關(guān)代碼和測(cè)試步驟,并提供了實(shí)際案例供參考,本文旨在幫助需要實(shí)現(xiàn)文件下載功能的開(kāi)發(fā)者快速掌握核心技術(shù)2024-10-10
springboot項(xiàng)目打成war包部署到tomcat遇到的一些問(wèn)題
這篇文章主要介紹了springboot項(xiàng)目打成war包部署到tomcat遇到的一些問(wèn)題,需要的朋友可以參考下2017-06-06

