SpringBoot配置主從數(shù)據(jù)庫(kù)實(shí)現(xiàn)讀寫分離
一、前言
現(xiàn)在的 Web 應(yīng)用大都是讀多寫少。除了緩存以外還可以通過(guò)數(shù)據(jù)庫(kù) “主從復(fù)制” 架構(gòu),把讀請(qǐng)求路由到從數(shù)據(jù)庫(kù)節(jié)點(diǎn)上,實(shí)現(xiàn)讀寫分離,從而大大提高應(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) “讀寫分離” 的場(chǎng)景不太適合。首先,多個(gè)數(shù)據(jù)源都是通過(guò) @Bean 定義的,當(dāng)需要新增額外的從數(shù)據(jù)庫(kù)時(shí)需要改動(dòng)代碼,非常不夠靈活。其次,在業(yè)務(wù)層中,如果需要根據(jù)讀、寫場(chǎng)景切換不同數(shù)據(jù)源的話只能手動(dòng)進(jìn)行。
對(duì)于 Spring Boot “讀寫分離” 架構(gòu)下的的多數(shù)據(jù)源,我們需要實(shí)現(xiàn)如下需求:
- 可以通過(guò)配置文件新增數(shù)據(jù)庫(kù)(從庫(kù)),而不不需要修改代碼。
- 自動(dòng)根據(jù)場(chǎng)景切換讀、寫數(shù)據(jù)源,對(duì)業(yè)務(wù)層是透明的。
幸運(yùn)的是,Spring Jdbc 模塊類提供了一個(gè) AbstractRoutingDataSource 抽象類可以實(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) “讀寫分離”。
二、實(shí)現(xiàn)思路
首先,創(chuàng)建自己的 AbstractRoutingDataSource 實(shí)現(xiàn)類。把它的默認(rèn)數(shù)據(jù)源 resolvedDefaultDataSource 設(shè)置為主庫(kù),從庫(kù)則保存到 Map<Object, DataSource> resolvedDataSources 中。
在 Spring Boot 應(yīng)用中通常使用 @Transactional 注解來(lái)開啟聲明式事務(wù),它的默認(rèn)傳播級(jí)別為 REQUIRED,也就是保證多個(gè)事務(wù)方法之間的相互調(diào)用都是在同一個(gè)事務(wù)中,使用的是同一個(gè) Jdbc 連接。它還有一個(gè) readOnly 屬性表示是否是只讀事務(wù)。
于是,我們可以通過(guò) AOP 技術(shù),在事務(wù)方法執(zhí)行之前,先獲取到方法上的 @Transactional 注解從而判斷是讀、還是寫業(yè)務(wù)。并且把 “讀寫狀態(tài)” 存儲(chǔ)到線程上下文(ThreadLocal)中!
在 AbstractRoutingDataSource 的 determineCurrentLookupKey 方法中,我們就可以根據(jù)當(dāng)前線程上下文中的 “讀寫狀態(tài)” 判斷當(dāng)前是否是只讀業(yè)務(wù),如果是,則返回從庫(kù) resolvedDataSources 中的 Key,反之則返回 null 表示使用默認(rèn)數(shù)據(jù)源也就是主庫(kù)。
三、初始化數(shù)據(jù)庫(kù)
首先,在本地創(chuàng)建 4 個(gè)不同名稱的數(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 表示名稱。
CREATE TABLE `test` ( `id` int NOT NULL COMMENT 'ID', `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '名稱', 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))依賴:
<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ù),也就是寫庫(kù)。然后在 app.datasource.slave 下以 Map 形式配置了多個(gè)從庫(kù)(也就是讀庫(kù)),每個(gè)從庫(kù)使用自定義的名稱作為 Key。
數(shù)據(jù)源的實(shí)現(xiàn)使用的是默認(rèn)的 HikariDataSource,并且數(shù)據(jù)源的配置是按照 HikariConfig 類定義的。也就是說(shuō),你可以根據(jù) HikariConfig 的屬性在配置中添加額外的設(shè)置。
有了配置后,還需要定義對(duì)應(yīng)的配置類,如下:
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 類上使用 @EnableConfigurationProperties 注解來(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}) // 指定要加載的配置類 public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }
這里還使用 @EnableAspectJAutoProxy 開啟了 AOP 的支持,后面會(huì)用到。
六、創(chuàng)建 MasterSlaveDataSourceMarker
創(chuàng)建一個(gè) MasterSlaveDataSourceMarker 類,用于維護(hù)當(dāng)前業(yè)務(wù)的 “讀寫狀態(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(); } // 寫狀態(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ù)的讀寫狀態(tài)。
如果 get() 返回 null 或者 true 則表示非只讀,需要使用主庫(kù)。反之則表示只讀業(yè)務(wù),使用從庫(kù)。
七、創(chuàng)建 MasterSlaveDataSourceAop
創(chuàng)建 MasterSlaveDataSourceAop 切面類,在事務(wù)方法開始之前執(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ù)開始之前執(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(); // 可寫,主庫(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)類 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ù)的 “讀寫狀態(tài)”,如果是只讀則通過(guò) AtomicInteger 原子類自增后從 slaveKeys 輪詢出一個(gè)從庫(kù)的 Key。反之則返回 null 使用主庫(kù)。
九、創(chuàng)建 MasterSlaveDataSourceConfiguration 配置類
最后,需要在 @Configuration 配置類中,創(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ò)配置方法注入配置類,該類定義了配置文件中的主庫(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ù)類。
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); } // 先讀,再寫 @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 類中定義了 2 個(gè)方法。
- read():只讀業(yè)務(wù),從表中檢索 name 字段返回。
- write:可寫業(yè)務(wù),先修改表中的 name 字段值為: new name,然后再調(diào)用 read() 方法讀取修改后的結(jié)果、返回。
2、創(chuàng)建測(cè)試類
創(chuàng)建測(cè)試類,如下:
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()); // 寫 log.info("write={}", this.testService.write()); } }
在測(cè)試類方法中,連續(xù)調(diào)用 4 次 TestService 的 read() 方法。由于這是一個(gè)只讀方法,按照我們的設(shè)定,它會(huì)在 3 個(gè)從庫(kù)之間輪詢使用。由于我們故意把三個(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í)輪詢了三個(gè)不同的從庫(kù),符合預(yù)期。最后的 write() 方法也成功地路由到了主庫(kù),執(zhí)行了修改并且返回了修改后的結(jié)果。
十一總結(jié)
通過(guò) AbstractRoutingDataSource 可以不使用任何第三方中間件就可以在 Spring Boot 中實(shí)現(xiàn)數(shù)據(jù)源 “讀寫分離”,這種方式需要在每個(gè)業(yè)務(wù)方法上通過(guò) @Transactional 注解明確定義是讀還是寫。
到此這篇關(guān)于SpringBoot配置主從數(shù)據(jù)庫(kù)實(shí)現(xiàn)讀寫分離的文章就介紹到這了,更多相關(guān)SpringBoot 讀寫分離內(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-11Java的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-03Quarkus改造Pmml模型項(xiàng)目異常記錄及解決處理
這篇文章主要為大家介紹了Quarkus改造Pmml模型項(xiàng)目是遇到的異常記錄以及解決方法,有需要的同學(xué)可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-02-02Java如何實(shí)現(xiàn)遠(yuǎn)程文件下載到本地目錄
本文介紹了如何使用Java來(lái)實(shí)現(xiàn)遠(yuǎn)程文件的下載功能,主要通過(guò)HTTPS路徑下載文件到本地目錄,詳細(xì)介紹了相關(guān)代碼和測(cè)試步驟,并提供了實(shí)際案例供參考,本文旨在幫助需要實(shí)現(xiàn)文件下載功能的開發(fā)者快速掌握核心技術(shù)2024-10-10springboot項(xiàng)目打成war包部署到tomcat遇到的一些問(wèn)題
這篇文章主要介紹了springboot項(xiàng)目打成war包部署到tomcat遇到的一些問(wèn)題,需要的朋友可以參考下2017-06-06