SpringBoot3實(shí)現(xiàn)MySQL讀寫分離的完整指南
讀寫分離的核心價(jià)值
在高并發(fā)場景下,數(shù)據(jù)庫往往成為系統(tǒng)瓶頸。讀寫分離通過將寫操作定向到主庫、讀操作分發(fā)到從庫,顯著提升系統(tǒng)讀性能和數(shù)據(jù)可用性。當(dāng)主庫出現(xiàn)故障時(shí),從庫可以繼續(xù)提供讀服務(wù),提高系統(tǒng)的穩(wěn)定性。
項(xiàng)目依賴配置
首先在pom.xml中添加必要依賴:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
</dependencies>
核心實(shí)現(xiàn)代碼詳解
1. 配置文件設(shè)置(application.yml)
spring:
datasource:
# 主庫配置(寫操作)
master:
jdbc-url: jdbc:mysql://localhost:3306/master_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: master_password
driver-class-name: com.mysql.cj.jdbc.Driver
# Hikari連接池配置
hikari:
maximum-pool-size: 10
minimum-idle: 5
idle-timeout: 30000
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
# 從庫配置(讀操作)
slave:
jdbc-url: jdbc:mysql://localhost:3306/slave_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: slave_password
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
maximum-pool-size: 15 # 從庫可以配置更多連接,因?yàn)樽x操作通常更頻繁
minimum-idle: 8
idle-timeout: 30000
max-lifetime: 1800000
connection-timeout: 30000
connection-test-query: SELECT 1
2. 數(shù)據(jù)源枚舉定義
/**
* 數(shù)據(jù)源類型枚舉
* 用于標(biāo)識當(dāng)前操作應(yīng)使用主庫還是從庫
*/
public enum DataSourceType {
MASTER, // 主庫:用于寫操作(INSERT、UPDATE、DELETE)
SLAVE // 從庫:用于讀操作(SELECT)
}
3. 數(shù)據(jù)源上下文管理器
/**
* 數(shù)據(jù)源上下文管理器(基于ThreadLocal實(shí)現(xiàn)線程隔離)
* 功能:保存當(dāng)前線程使用的數(shù)據(jù)源類型,確保多線程環(huán)境下數(shù)據(jù)源切換不會(huì)相互干擾
*/
public class DataSourceContextHolder {
// 使用ThreadLocal保證線程安全,每個(gè)線程有獨(dú)立的數(shù)據(jù)源上下文
private static final ThreadLocal<DataSourceType> CONTEXT_HOLDER = new ThreadLocal<>();
/**
* 設(shè)置當(dāng)前線程的數(shù)據(jù)源類型
* @param dataSourceType 數(shù)據(jù)源類型(MASTER或SLAVE)
*/
public static void setDataSourceType(DataSourceType dataSourceType) {
CONTEXT_HOLDER.set(dataSourceType);
}
/**
* 獲取當(dāng)前線程的數(shù)據(jù)源類型
* @return 當(dāng)前數(shù)據(jù)源類型,默認(rèn)為MASTER(保證寫操作可靠性)
*/
public static DataSourceType getDataSourceType() {
return CONTEXT_HOLDER.get() == null ? DataSourceType.MASTER : CONTEXT_HOLDER.get();
}
/**
* 清除當(dāng)前線程的數(shù)據(jù)源類型
* 防止內(nèi)存泄漏,特別是在線程池場景下
*/
public static void clearDataSourceType() {
CONTEXT_HOLDER.remove();
}
}
4. 動(dòng)態(tài)路由數(shù)據(jù)源
/**
* 動(dòng)態(tài)路由數(shù)據(jù)源(繼承Spring的AbstractRoutingDataSource)
* 核心功能:根據(jù)當(dāng)前上下文動(dòng)態(tài)選擇主庫或從庫
*/
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
/**
* 決定當(dāng)前數(shù)據(jù)源查找鍵(Spring在每次數(shù)據(jù)庫操作前調(diào)用此方法)
* @return 數(shù)據(jù)源查找鍵(MASTER或SLAVE)
*/
@Override
protected Object determineCurrentLookupKey() {
DataSourceType dataSourceType = DataSourceContextHolder.getDataSourceType();
System.out.println("當(dāng)前使用的數(shù)據(jù)源: " + dataSourceType);
return dataSourceType;
}
}
5. 數(shù)據(jù)源配置類
/**
* 數(shù)據(jù)源配置類(核心配置)
* 配置主從數(shù)據(jù)源并初始化路由數(shù)據(jù)源
*/
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
basePackages = "com.example.repository",
entityManagerFactoryRef = "entityManagerFactory",
transactionManagerRef = "transactionManager"
)
public class DataSourceConfig {
/**
* 主庫數(shù)據(jù)源(寫操作)
*/
@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
/**
* 從庫數(shù)據(jù)源(讀操作)
*/
@Bean(name = "slaveDataSource")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create().build();
}
/**
* 動(dòng)態(tài)路由數(shù)據(jù)源(優(yōu)先級最高,作為主數(shù)據(jù)源)
*/
@Primary
@Bean(name = "routingDataSource")
public DataSource routingDataSource(
@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
DynamicRoutingDataSource routingDataSource = new DynamicRoutingDataSource();
// 配置目標(biāo)數(shù)據(jù)源映射
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceType.MASTER, masterDataSource);
targetDataSources.put(DataSourceType.SLAVE, slaveDataSource);
routingDataSource.setTargetDataSources(targetDataSources);
routingDataSource.setDefaultTargetDataSource(masterDataSource); // 默認(rèn)使用主庫
return routingDataSource;
}
/**
* 實(shí)體管理器工廠
*/
@Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
EntityManagerFactoryBuilder builder,
@Qualifier("routingDataSource") DataSource dataSource) {
return builder
.dataSource(dataSource)
.packages("com.example.entity")
.persistenceUnit("mysqlUnit")
.build();
}
/**
* 事務(wù)管理器
*/
@Bean(name = "transactionManager")
public PlatformTransactionManager transactionManager(
@Qualifier("entityManagerFactory") EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
6. AOP切面實(shí)現(xiàn)自動(dòng)路由
/**
* 數(shù)據(jù)源切面配置(基于AOP自動(dòng)切換數(shù)據(jù)源)
* 通過方法名自動(dòng)識別讀寫操作,實(shí)現(xiàn)數(shù)據(jù)源動(dòng)態(tài)路由
*/
@Aspect
@Component
@Order(1) // 確保在事務(wù)切面之前執(zhí)行
public class DataSourceAspect {
/**
* 寫操作切點(diǎn)(insert、update、delete、save開頭的方法)
*/
@Before("execution(* com.example.service..*.create*(..)) || " +
"execution(* com.example.service..*.update*(..)) || " +
"execution(* com.example.service..*.delete*(..)) || " +
"execution(* com.example.service..*.save*(..))")
public void setWriteDataSourceType() {
DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
System.out.println("切換到主庫(寫操作)");
}
/**
* 讀操作切點(diǎn)(select、get、find、query開頭的方法)
*/
@Before("execution(* com.example.service..*.select*(..)) || " +
"execution(* com.example.service..*.get*(..)) || " +
"execution(* com.example.service..*.find*(..)) || " +
"execution(* com.example.service..*.query*(..))")
public void setReadDataSourceType() {
DataSourceContextHolder.setDataSourceType(DataSourceType.SLAVE);
System.out.println("切換到從庫(讀操作)");
}
/**
* 后置處理:清理數(shù)據(jù)源上下文
*/
@After("execution(* com.example.service..*.*(..))")
public void clearDataSourceType() {
DataSourceContextHolder.clearDataSourceType();
System.out.println("清理數(shù)據(jù)源上下文");
}
}
7. 業(yè)務(wù)層使用示例
/**
* 用戶服務(wù)實(shí)現(xiàn)類
* 演示讀寫分離的實(shí)際應(yīng)用
*/
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
/**
* 新增用戶(寫操作自動(dòng)路由到主庫)
*/
@Override
public User createUser(User user) {
// 方法名以"create"開頭,AOP會(huì)自動(dòng)切換到MASTER數(shù)據(jù)源
return userRepository.save(user);
}
/**
* 根據(jù)ID查詢用戶(讀操作自動(dòng)路由到從庫)
*/
@Override
@Transactional(readOnly = true) // 只讀事務(wù)優(yōu)化性能
public User getUserById(Long id) {
// 方法名以"get"開頭,AOP會(huì)自動(dòng)切換到SLAVE數(shù)據(jù)源
return userRepository.findById(id).orElse(null);
}
/**
* 查詢所有用戶(讀操作)
*/
@Override
@Transactional(readOnly = true)
public List<User> getAllUsers() {
return userRepository.findAll();
}
/**
* 更新用戶信息(寫操作)
*/
@Override
public User updateUser(User user) {
return userRepository.save(user);
}
}
測試與驗(yàn)證
單元測試類
/**
* 讀寫分離測試類
*/
@SpringBootTest
class ReadWriteSeparationTest {
@Autowired
private UserService userService;
/**
* 測試寫操作(應(yīng)路由到主庫)
*/
@Test
void testWriteOperation() {
User user = new User();
user.setUsername("testUser");
user.setPassword("password");
User savedUser = userService.createUser(user);
Assertions.assertNotNull(savedUser.getId());
System.out.println("寫操作測試通過(路由到主庫)");
}
/**
* 測試讀操作(應(yīng)路由到從庫)
*/
@Test
void testReadOperation() {
List<User> users = userService.getAllUsers();
Assertions.assertNotNull(users);
System.out.println("讀操作測試通過(路由到從庫)");
}
/**
* 測試讀寫混合操作
*/
@Test
void testReadWriteMix() {
// 寫操作
User user = new User();
user.setUsername("mixUser");
userService.createUser(user);
// 讀操作
User foundUser = userService.getUserById(1L);
Assertions.assertNotNull(foundUser);
System.out.println("讀寫混合操作測試通過");
}
}
關(guān)鍵注意事項(xiàng)
1. 主從同步延遲處理
在讀寫分離架構(gòu)中,主從同步存在延遲可能性。剛寫入主庫的數(shù)據(jù)可能不會(huì)立即在從庫中可用。
解決方案:
/**
* 強(qiáng)制讀主庫的場景
*/
@Service
public class CriticalService {
@Autowired
private UserRepository userRepository;
/**
* 重要業(yè)務(wù):寫入后立即讀取,強(qiáng)制走主庫
*/
public User createAndGetUser(User user) {
// 寫入主庫
User savedUser = userRepository.save(user);
// 強(qiáng)制從主庫讀?。ū苊馔窖舆t)
DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
try {
return userRepository.findById(savedUser.getId()).orElse(null);
} finally {
DataSourceContextHolder.clearDataSourceType();
}
}
}
2. 事務(wù)中的數(shù)據(jù)處理
在事務(wù)中,所有操作應(yīng)使用同一數(shù)據(jù)源。
解決方案:
@Service
public class TransactionalService {
/**
* 事務(wù)內(nèi)強(qiáng)制使用主庫
*/
@Transactional
public void complexBusinessOperation() {
// 方法開始時(shí)顯式設(shè)置主庫,確保事務(wù)內(nèi)一致性
DataSourceContextHolder.setDataSourceType(DataSourceType.MASTER);
try {
// 一系列數(shù)據(jù)庫操作...
// 所有這些操作都在同一事務(wù)中,使用同一數(shù)據(jù)源
} finally {
// 事務(wù)結(jié)束后清理
DataSourceContextHolder.clearDataSourceType();
}
}
}
方案優(yōu)缺點(diǎn)分析
| 優(yōu)勢 | 挑戰(zhàn) | 應(yīng)對策略 |
|---|---|---|
| 提升讀性能:將讀請求分發(fā)到從庫 | 主從同步延遲 | 關(guān)鍵業(yè)務(wù)強(qiáng)制讀主庫 |
| 提高可用性:主庫故障時(shí)從庫可讀 | 事務(wù)內(nèi)數(shù)據(jù)源一致性 | 事務(wù)中強(qiáng)制使用主庫 |
| 減輕主庫壓力 | 復(fù)雜SQL路由 | 明確的讀寫操作分離 |
總結(jié)
通過以上完整的Spring Boot 3實(shí)現(xiàn)方案,你可以成功配置MySQL讀寫分離。關(guān)鍵在于理解動(dòng)態(tài)數(shù)據(jù)源路由原理,合理處理主從同步延遲和事務(wù)一致性等挑戰(zhàn)。這種架構(gòu)能顯著提升系統(tǒng)性能,特別適合讀多寫少的應(yīng)用場景。
以上就是SpringBoot3實(shí)現(xiàn)MySQL讀寫分離的完整指南的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot3 MySQL讀寫分離的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java覆蓋第三方j(luò)ar包中的某一個(gè)類的實(shí)現(xiàn)方法
在我們?nèi)粘5拈_發(fā)中,經(jīng)常需要使用第三方的 jar 包,有時(shí)候我們會(huì)發(fā)現(xiàn)第三方的 jar 包中的某一個(gè)類有問題,或者我們需要定制化修改其中的邏輯,那么應(yīng)該如何實(shí)現(xiàn)呢,本文給大家介紹了Java覆蓋第三方j(luò)ar包中的某一個(gè)類的實(shí)現(xiàn)方法,需要的朋友可以參考下2025-02-02
Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(22)
下面小編就為大家?guī)硪黄狫ava基礎(chǔ)的幾道練習(xí)題(分享)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧,希望可以幫到你2021-07-07
Java中Socket設(shè)置連接超時(shí)的代碼分享
在我們?nèi)粘_B接中,如果超時(shí)時(shí)長過長的話,在開發(fā)時(shí)會(huì)影響測試,下面這篇文章主要給大家分享了關(guān)于Java中Socket設(shè)置連接超時(shí)的代碼,需要的朋友可以參考借鑒,下面來一起看看吧。2017-06-06
Java并發(fā)系列之AbstractQueuedSynchronizer源碼分析(概要分析)
這篇文章主要為大家詳細(xì)介紹了Java并發(fā)系列之AbstractQueuedSynchronizer源碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-02-02
IDEA設(shè)置maven修改settings.xml配置文件無法加載倉庫的解決方案
這篇文章主要介紹了IDEA設(shè)置maven修改settings.xml配置文件無法加載倉庫的解決方案,幫助大家更好的利用IDEA進(jìn)行JAVA的開發(fā)學(xué)習(xí),感興趣的朋友可以了解下2021-01-01
Mybatis的Mapper代理對象生成及調(diào)用過程示例詳解
這篇文章主要為大家介紹了Mybatis的Mapper代理對象生成及調(diào)用過程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09
Java實(shí)現(xiàn)JWT 雙簽發(fā)認(rèn)證+RBAC權(quán)限的示例代碼
本文主要介紹了Java實(shí)現(xiàn)JWT 雙簽發(fā)認(rèn)證+RBAC權(quán)限的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2025-08-08

