Spring Boot + MyBatis-Plus 實現(xiàn) MySQL 主從復(fù)制動態(tài)數(shù)據(jù)源切換
MySQL 主從復(fù)制是一種常見的數(shù)據(jù)庫架構(gòu),它可以提高數(shù)據(jù)庫的性能和可用性。動態(tài)數(shù)據(jù)源切換則可以根據(jù)業(yè)務(wù)需求,在不同場景下使用不同的數(shù)據(jù)源,比如在讀多寫少的場景下,可以通過切換到從庫來分擔(dān)主庫的壓力。
在本文中,我們將介紹如何在 Spring Boot 中實現(xiàn) MySQL 主從復(fù)制和動態(tài)數(shù)據(jù)源切換,使用 MyBatis-Plus 進(jìn)行數(shù)據(jù)庫操作
#代碼地址
https://github.com/bangbangzhou/spring-boot-dynamic-master-slave.git
今日內(nèi)容介紹,大約花費(fèi)19分鐘
圖片
那么接下來我們開始項目實現(xiàn),項目結(jié)構(gòu)如下
圖片
1.引入依賴
在項目的的pom.xml文件中引入Spring Boot和MyBatis-Plus的相關(guān)依賴
<?xml versinotallow="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>spring-boot-starter-parent</artifactId>
<groupId>org.springframework.boot</groupId>
<version>2.7.15</version>
</parent>
<groupId>com.zbbmeta</groupId>
<artifactId>spring-boot-dynamic-master-slave</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.20</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
2. 配置數(shù)據(jù)源
在application.yml文件中配置主從數(shù)據(jù)源信息。注意這里我們要搭建主從數(shù)據(jù)庫,只是在一個mysql實例中創(chuàng)建兩個庫,里面存在相同表
server:
port: 8082
spring:
datasource:
master:
username: root
password: root
url: jdbc:mysql://localhost:3306/shiro_db?useUnicode=true&characterEncoding=utf8
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
username: root
password: root
url: jdbc:mysql://localhost:3306/backend_db?useUnicode=true&characterEncoding=utf8
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
3. 創(chuàng)建DatabaseType 枚舉類型
創(chuàng)建DatabaseType 枚舉類型,用于切換數(shù)據(jù)源時,確定連接的是哪個數(shù)據(jù)源
在com.zbbmeta.config包下創(chuàng)建DatabaseType枚舉類型
// 定義一個枚舉類型 DatabaseType,表示系統(tǒng)中的數(shù)據(jù)庫類型
public enum DatabaseType {
MASTER, // 主數(shù)據(jù)庫類型
SLAVE // 從數(shù)據(jù)庫類型
}
4. 配置數(shù)據(jù)源上下文
在com.zbbmeta.holder包下創(chuàng)建一個DataSourceContextHolder類用于保存和獲取當(dāng)前線程使用的數(shù)據(jù)源類型
public class DatabaseContextHolder {
private static final ThreadLocal<DatabaseType> contextHolder = new ThreadLocal<>();
public static void setDatabaseType(DatabaseType databaseType) {
contextHolder.set(databaseType);
}
public static DatabaseType getDatabaseType() {
return contextHolder.get();
}
public static void clearDatabaseType() {
contextHolder.remove();
}
}
5. 配置動態(tài)數(shù)據(jù)源
我們創(chuàng)建了一個 DynamicDataSource 類,繼承 AbstractRoutingDataSource,用于實現(xiàn)動態(tài)數(shù)據(jù)源的切換。
AbstractRoutingDataSource 是 Spring Framework 提供的一個抽象數(shù)據(jù)源類,用于實現(xiàn)動態(tài)數(shù)據(jù)源切換。它允許應(yīng)用程序在運(yùn)行時動態(tài)地切換到不同的數(shù)據(jù)源,從而支持多數(shù)據(jù)源的場景,比如數(shù)據(jù)庫讀寫分離、主從復(fù)制等
AbstractRoutingDataSource介紹:
- 動態(tài)數(shù)據(jù)源切換:AbstractRoutingDataSource 的核心思想是根據(jù)某個鍵值(lookup key)來決定使用哪個具體的數(shù)據(jù)源。這個鍵值是通過 determineCurrentLookupKey() 方法提供
- 抽象類:AbstractRoutingDataSource 是一個抽象類,它提供了模板方法 determineCurrentLookupKey(),需要由子類實現(xiàn)
- 實現(xiàn) javax.sql.DataSource 接口:AbstractRoutingDataSource 實現(xiàn)了 javax.sql.DataSource 接口,因此可以像常規(guī)數(shù)據(jù)源一樣被用于與數(shù)據(jù)庫的交互。
- 在 Spring 配置中使用:在 Spring 的配置中,我們可以將 AbstractRoutingDataSource 配置為數(shù)據(jù)源 bean,并將真實的數(shù)據(jù)源作為其目標(biāo)數(shù)據(jù)源。在需要切換數(shù)據(jù)源的時候,調(diào)用 determineCurrentLookupKey() 方法,它將返回用于切換數(shù)據(jù)源的鍵值。
在com.zbbmeta.config包下創(chuàng)建DynamicDataSource類
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDataSourceType();
}
}
DynamicDataSource類中重寫determineCurrentLookupKey()方法:在這個方法中,我們通過調(diào)用 DataSourceContextHolder.getDataSourceType() 來獲取當(dāng)前線程持有的數(shù)據(jù)源類型。這個方法的返回值將被用作數(shù)據(jù)源的 lookup key,從而實現(xiàn)動態(tài)切換。
6. 添加DataSource注解類
在·com.zbbmeta.annotation包下創(chuàng)建DataSource注解類,這是一個自定義注解,用于標(biāo)記在類或方法上,以指定數(shù)據(jù)源的類型。下面是對這段代碼的注解說明
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataSource {
DatabaseType type() default DatabaseType.SLAVE;
}
注解說明:
- @interface DataSource:這是一個注解的聲明,用于創(chuàng)建名為 DataSource 的自定義注解。
- @Target({ElementType.METHOD, ElementType.TYPE}):@Target 注解表示此注解可以用于類和方法。在這里,DataSource 注解可以標(biāo)注在類和方法上。
- @Retention(RetentionPolicy.RUNTIME):@Retention 注解表示這個注解的生命周期,即在運(yùn)行時仍然可用。這是因為我們希望在運(yùn)行時通過反射獲取注解信息。
- DatabaseType type() default DatabaseType.SLAVE:這是 DataSource 注解的一個成員變量。它是一個枚舉類型的變量,表示數(shù)據(jù)庫類型,默認(rèn)值為 SLAVE。通過這個成員變量,我們可以在使用 DataSource 注解時指定使用的數(shù)據(jù)源類型
7. 配置數(shù)據(jù)源切換切面
在com.zbbmeta.aspect包下創(chuàng)建一個切面類DataSourceAspect,用于在執(zhí)行數(shù)據(jù)庫操作前動態(tài)切換數(shù)據(jù)源。
@Aspect
@Component
@EnableAspectJAutoProxy
public class DataSourceAspect {
// 定義切點(diǎn),匹配使用了 @DataSource 注解的方法
@Pointcut("@annotation(com.zbbmeta.annotation.DataSource)")
public void dataSourcePointCut() {}
// 環(huán)繞通知,在方法執(zhí)行前后切換數(shù)據(jù)源
@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
// 獲取方法上的 @DataSource 注解
DataSource dataSource = method.getAnnotation(DataSource.class);
if (dataSource != null) {
// 切換數(shù)據(jù)源類型
DatabaseContextHolder.setDatabaseType(dataSource.type());
}
try {
// 執(zhí)行目標(biāo)方法
return point.proceed();
} finally {
// 清除數(shù)據(jù)源類型,確保線程安全
DatabaseContextHolder.clearDatabaseType();
}
}
}
8. 創(chuàng)建DataSourceConfig
在com.zbbmeta.config包下創(chuàng)建DataSourceConfig,用于配置主從兩個數(shù)據(jù)源
@Configuration
@Data
public class DataSourceConfig {
@Value("${spring.datasource.master.url}")
private String dbUrl;
@Value("${spring.datasource.master.username}")
private String username;
@Value("${spring.datasource.master.password}")
private String password;
@Value("${spring.datasource.master.driver-class-name}")
private String driverClassName;
@Value("${spring.datasource.slave.url}")
private String slaveDbUrl;
@Value("${spring.datasource.slave.username}")
private String slaveUsername;
@Value("${spring.datasource.slave.password}")
private String slavePassword;
@Value("${spring.datasource.slave.driver-class-name}")
private String slaveDriverClassName;
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create()
.driverClassName(driverClassName)
.url(dbUrl)
.username(username)
.password(password)
.build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource slaveDataSource() {
return DataSourceBuilder.create()
.driverClassName(slaveDriverClassName)
.url(slaveDbUrl)
.username(slaveUsername)
.password(slavePassword)
.build();
}
}
9 創(chuàng)建DataSourceConfig
在com.zbbmeta.config包下創(chuàng)建DynamicDataSourceConfig類中配置MyBatis-Plus的相關(guān)內(nèi)容。
@Configuration
@MapperScan("com.zbbmeta.mapper")
public class DynamicDataSourceConfig {
@Autowired
private DataSource masterDataSource;
@Autowired
private DataSource slaveDataSource;
// 配置動態(tài)數(shù)據(jù)源
@Bean
@Primary
public DataSource dynamicDataSource() {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DatabaseType.MASTER, masterDataSource);
targetDataSources.put(DatabaseType.SLAVE, slaveDataSource);
DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
dynamicDataSource.setTargetDataSources(targetDataSources);
dynamicDataSource.setDefaultTargetDataSource(masterDataSource); // 設(shè)置默認(rèn)數(shù)據(jù)源
return dynamicDataSource;
}
// 配置 MyBatis 的 SqlSessionFactory
@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dynamicDataSource) throws Exception {
MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dynamicDataSource);
// 設(shè)置要掃描的 mapper 接口和 XML 文件路徑
sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
sessionFactoryBean.setTypeAliasesPackage("com.zbbmeta.entity"); // 設(shè)置實體類包路徑
return sessionFactoryBean.getObject();
}
// 配置 MyBatis 的 SqlSessionTemplate
@Bean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
10. 測試
使用MybatisX生成代碼,并且創(chuàng)建com.zbbmeta.controller包下創(chuàng)建TutorialController類,并且在需要切換數(shù)據(jù)源的方法上使用 @DataSource 注解,切面將根據(jù)該注解的配置在方法執(zhí)行前后進(jìn)行數(shù)據(jù)源切換。
圖片
圖片
@RestController
public class TutorialController {
@Autowired
private TutorialService tutorialService;
@DataSource
@GetMapping("/list")
public List<Tutorial> list(){
return tutorialService.list();
}
@DataSource(type = DatabaseType.MASTER)
@GetMapping("/create")
public Boolean create(){
Tutorial tutorial = new Tutorial();
tutorial.setTitle("master");
tutorial.setDescription("master");
return tutorialService.save(tutorial);
}
}
使用POSTMAN發(fā)送請求
http://localhost:8082/list
http://localhost:8082/create
#代碼地址
https://github.com/bangbangzhou/spring-boot-dynamic-master-slave.git