Spring Boot + MyBatis-Plus 实现 MySQL 主从复制动态数据源切换 ...

打印 上一主题 下一主题

主题 917|帖子 917|积分 2751

MySQL 主从复制是一种常见的数据库架构,它可以提高数据库的性能和可用性。动态数据源切换则可以根据业务需求,在不同场景下使用不同的数据源,比如在读多写少的场景下,可以通过切换到从库来分担主库的压力
在本文中,我们将介绍如何在 Spring Boot 中实现 MySQL 动态数据源切换,使用 MyBatis-Plus 进行数据库操作
那么接下来我们开始项目实现,项目结构如下

前备:可以提前导入sql
  1. create table tb_tutorial
  2. (
  3.     id          bigint auto_increment comment '主键ID'
  4.         primary key,
  5.     title       varchar(40) null comment '标题',
  6.     description varchar(30) null comment '描述',
  7.     published   tinyint     null comment '1 表示发布 0 表示未发布'
  8. );
  9. -- 在从库进行数据添加
  10. INSERT INTO user_db.tb_tutorial
  11. (id, title, description, published)
  12. VALUES(1758812356898889, 'savle', 'savle', 1);
复制代码
1.引入依赖

在项目的的pom.xml文件中引入Spring Boot和MyBatis-Plus的相关依赖
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3.          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4.          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5.     <modelVersion>4.0.0</modelVersion>
  6.     <parent>
  7.         <artifactId>spring-boot-starter-parent</artifactId>
  8.         <groupId>org.springframework.boot</groupId>
  9.         <version>2.7.15</version>
  10.     </parent>
  11.     <groupId>com.zbbmeta</groupId>
  12.     <artifactId>spring-boot-dynamic-master-slave</artifactId>
  13.     <version>1.0-SNAPSHOT</version>
  14.     <properties>
  15.         <maven.compiler.source>11</maven.compiler.source>
  16.         <maven.compiler.target>11</maven.compiler.target>
  17.     </properties>
  18.     <dependencies>
  19.         <dependency>
  20.             <groupId>org.springframework.boot</groupId>
  21.             <artifactId>spring-boot-starter-web</artifactId>
  22.         </dependency>
  23.         <dependency>
  24.             <groupId>mysql</groupId>
  25.             <artifactId>mysql-connector-java</artifactId>
  26.             <version>8.0.30</version>
  27.         </dependency>
  28.         <dependency>
  29.             <groupId>com.baomidou</groupId>
  30.             <artifactId>mybatis-plus-boot-starter</artifactId>
  31.             <version>3.5.3</version>
  32.         </dependency>
  33.         <dependency>
  34.             <groupId>cn.hutool</groupId>
  35.             <artifactId>hutool-all</artifactId>
  36.             <version>5.8.20</version>
  37.         </dependency>
  38.         <dependency>
  39.             <groupId>org.projectlombok</groupId>
  40.             <artifactId>lombok</artifactId>
  41.         </dependency>
  42.         <dependency>
  43.             <groupId>org.springframework.boot</groupId>
  44.             <artifactId>spring-boot-starter-aop</artifactId>
  45.         </dependency>
  46.     </dependencies>
  47.     <build>
  48.         <plugins>
  49.             <plugin>
  50.                 <groupId>org.springframework.boot</groupId>
  51.                 <artifactId>spring-boot-maven-plugin</artifactId>
  52.                 <configuration>
  53.                     <excludes>
  54.                         <exclude>
  55.                             <groupId>org.projectlombok</groupId>
  56.                             <artifactId>lombok</artifactId>
  57.                         </exclude>
  58.                     </excludes>
  59.                 </configuration>
  60.             </plugin>
  61.         </plugins>
  62.     </build>
  63. </project>
复制代码
2. 配置数据源

在application.yml文件中配置主从数据源信息。注意这里我们要搭建主从数据库,本文只是在一个mysql实例中创建两个库,里面存在相同表
正确应该是两个不同的mysql实例,作者暂时没有环境
  1. server:
  2.   port: 8082
  3. spring:
  4.   datasource:
  5.     master:
  6.       username: root
  7.       password: root
  8.       url: jdbc:mysql://localhost:3306/webapi?useUnicode=true&characterEncoding=utf8
  9.       driver-class-name: com.mysql.cj.jdbc.Driver
  10.     slave:
  11.       username: root
  12.       password: root
  13.       url: jdbc:mysql://localhost:3306/user_db?useUnicode=true&characterEncoding=utf8
  14.       driver-class-name: com.mysql.cj.jdbc.Driver
  15. mybatis-plus:
  16.   mapper-locations: classpath*:/mapper/**/*.xml
复制代码
3. 创建DatabaseType 枚举类型

创建DatabaseType 枚举类型,用于切换数据源时,确定连接的是那个数据源
在com.zbbmeta.config包下创建DatabaseType枚举类型
  1. // 定义一个枚举类型 DatabaseType,表示系统中的数据库类型
  2. public enum DatabaseType {
  3.     MASTER,  // 主数据库类型
  4.     SLAVE    // 从数据库类型
  5. }
复制代码
4. 配置数据源上下文

在com.zbbmeta.holder包下创建一个DataSourceContextHolder类用于保存和获取当前线程使用的数据源类型
  1. public class DatabaseContextHolder {
  2.     private static final ThreadLocal<DatabaseType> contextHolder = new ThreadLocal<>();
  3.     public static void setDatabaseType(DatabaseType databaseType) {
  4.         contextHolder.set(databaseType);
  5.     }
  6.     public static DatabaseType getDatabaseType() {
  7.         return contextHolder.get();
  8.     }
  9.     public static void clearDatabaseType() {
  10.         contextHolder.remove();
  11.     }
  12. }
复制代码
5. 配置动态数据源

我们创建了一个 DynamicDataSource 类,继承 AbstractRoutingDataSource,用于实现动态数据源的切换。
AbstractRoutingDataSource 是 Spring Framework 提供的一个抽象数据源类,用于实现动态数据源切换它允许应用程序在运行时动态地切换到不同的数据源,从而支持多数据源的场景,比如数据库读写分离、主从复制等
AbstractRoutingDataSource介绍:

  • 动态数据源切换: AbstractRoutingDataSource 的核心思想是根据某个键值(lookup key)来决定使用哪个具体的数据源。这个键值是通过 determineCurrentLookupKey() 方法提供
  • 抽象类: AbstractRoutingDataSource 是一个抽象类,它提供了模板方法 determineCurrentLookupKey(),需要由子类实现
  • 实现 javax.sql.DataSource 接口: AbstractRoutingDataSource 实现了 javax.sql.DataSource 接口,因此可以像常规数据源一样被用于与数据库的交互。
  • 在 Spring 配置中使用: 在 Spring 的配置中,我们可以将 AbstractRoutingDataSource 配置为数据源 bean,并将真实的数据源作为其目标数据源。在需要切换数据源的时候,调用 determineCurrentLookupKey() 方法,它将返回用于切换数据源的键值。
在com.zbbmeta.config包下创建DynamicDataSource类
  1. public class DynamicDataSource extends AbstractRoutingDataSource {
  2.     @Override
  3.     protected Object determineCurrentLookupKey() {
  4.         // 从数据源上下文获取数据源类型
  5.         return DataSourceContextHolder.getDataSourceType();
  6.     }
  7. }
复制代码
DynamicDataSource类中重写determineCurrentLookupKey()方法: 在这个方法中,我们通过调用 DataSourceContextHolder.getDataSourceType() 来获取当前线程持有的数据源类型。这个方法的返回值将被用作数据源的 lookup key,从而实现动态切换。
6. 添加DataSource注解类

在·com.zbbmeta.annotation包下创建DataSource注解类,这是一个自定义注解,用于标记在类或方法上,以指定数据源的类型。下面是对这段代码的注解说明
  1. @Target({ ElementType.METHOD, ElementType.TYPE })
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Documented
  4. public @interface DataSource {
  5.     // 默认是从数据库
  6.     DatabaseType type() default DatabaseType.SLAVE;
  7. }
复制代码
注解说明

  • @interface DataSource: 这是一个注解的声明,用于创建名为 DataSource 的自定义注解。
  • @Target({ElementType.METHOD, ElementType.TYPE}): @Target 注解表示此注解可以用于类和方法。在这里,DataSource 注解可以标注在类和方法上。
  • @Retention(RetentionPolicy.RUNTIME): @Retention 注解表示这个注解的生命周期,即在运行时仍然可用。这是因为我们希望在运行时通过反射获取注解信息。
  • DatabaseType type() default DatabaseType.SLAVE: 这是 DataSource 注解的一个成员变量。它是一个枚举类型的变量,表示数据库类型,默认值为 SLAVE。通过这个成员变量,我们可以在使用 DataSource 注解时指定使用的数据源类型
7. 配置数据源切换切面

在com.zbbmeta.aspect报下创建一个切面类DataSourceAspect,用于在执行数据库操作前动态切换数据源。
  1. @Aspect
  2. @Component
  3. @EnableAspectJAutoProxy
  4. public class DataSourceAspect {
  5. // 定义切点,匹配使用了 @DataSource 注解的方法
  6.     @Pointcut("@annotation(com.zbbmeta.annotation.DataSource)")
  7.     public void dataSourcePointCut() {}
  8.     // 环绕通知,在方法执行前后切换数据源
  9.     @Around("dataSourcePointCut()")
  10.     public Object around(ProceedingJoinPoint point) throws Throwable {
  11.         MethodSignature signature = (MethodSignature) point.getSignature();
  12.         Method method = signature.getMethod();
  13.         // 获取方法上的 @DataSource 注解
  14.         DataSource dataSource = method.getAnnotation(DataSource.class);
  15.         if (dataSource != null) {
  16.             // 切换数据源类型
  17.             DatabaseContextHolder.setDatabaseType(dataSource.type());
  18.         }
  19.         try {
  20.             // 执行目标方法
  21.             return point.proceed();
  22.         } finally {
  23.             // 清除数据源类型,确保线程安全
  24.             DatabaseContextHolder.clearDatabaseType();
  25.         }
  26.     }
  27. }
复制代码
8. 创建DataSourceConfig

在com.zbbmeta.config包下创建DataSourceConfig,用于配置主从两个数据源
  1. @Configuration
  2. @Data
  3. public class DataSourceConfig {
  4.     @Value("${spring.datasource.master.url}")
  5.     private String dbUrl;
  6.     @Value("${spring.datasource.master.username}")
  7.     private String username;
  8.     @Value("${spring.datasource.master.password}")
  9.     private String password;
  10.     @Value("${spring.datasource.master.driver-class-name}")
  11.     private String driverClassName;
  12.     @Value("${spring.datasource.slave.url}")
  13.     private String slaveDbUrl;
  14.     @Value("${spring.datasource.slave.username}")
  15.     private String slaveUsername;
  16.     @Value("${spring.datasource.slave.password}")
  17.     private String slavePassword;
  18.     @Value("${spring.datasource.slave.driver-class-name}")
  19.     private String slaveDriverClassName;
  20.     @Bean
  21.     @ConfigurationProperties(prefix = "spring.datasource.master")
  22.     public DataSource masterDataSource() {
  23.         return DataSourceBuilder.create()
  24.                 .driverClassName(driverClassName)
  25.                 .url(dbUrl)
  26.                 .username(username)
  27.                 .password(password)
  28.                 .build();
  29.     }
  30.     @Bean
  31.     @ConfigurationProperties(prefix = "spring.datasource.slave")
  32.     public DataSource slaveDataSource() {
  33.         return DataSourceBuilder.create()
  34.                 .driverClassName(slaveDriverClassName)
  35.                 .url(slaveDbUrl)
  36.                 .username(slaveUsername)
  37.                 .password(slavePassword)
  38.                 .build();
  39.     }
  40. }
复制代码
9 创建DataSourceConfig

在com.zbbmeta.config包下创建DynamicDataSourceConfig类中配置MyBatis-Plus的相关内容。
  1. @Configuration
  2. @MapperScan("com.zbbmeta.mapper")
  3. public class DynamicDataSourceConfig {
  4.     @Autowired
  5.     private DataSource masterDataSource;
  6.     @Autowired
  7.     private DataSource slaveDataSource;
  8.     // 配置动态数据源
  9.     @Bean
  10.     @Primary
  11.     public DataSource dynamicDataSource() {
  12.         Map<Object, Object> targetDataSources = new HashMap<>();
  13.         targetDataSources.put(DatabaseType.MASTER, masterDataSource);
  14.         targetDataSources.put(DatabaseType.SLAVE, slaveDataSource);
  15.         DynamicRoutingDataSource dynamicDataSource = new DynamicRoutingDataSource();
  16.         dynamicDataSource.setTargetDataSources(targetDataSources);
  17.         dynamicDataSource.setDefaultTargetDataSource(masterDataSource); // 设置默认数据源
  18.         return dynamicDataSource;
  19.     }
  20.     // 配置 MyBatis 的 SqlSessionFactory
  21.     @Bean
  22.     public SqlSessionFactory sqlSessionFactory(DataSource dynamicDataSource) throws Exception {
  23.         MybatisSqlSessionFactoryBean sessionFactoryBean = new MybatisSqlSessionFactoryBean();
  24.         sessionFactoryBean.setDataSource(dynamicDataSource);
  25.         // 设置要扫描的 mapper 接口和 XML 文件路径
  26.         sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
  27.         sessionFactoryBean.setTypeAliasesPackage("com.zbbmeta.entity");  // 设置实体类包路径
  28.         return sessionFactoryBean.getObject();
  29.     }
  30.     // 配置 MyBatis 的 SqlSessionTemplate
  31.     @Bean
  32.     public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
  33.         return new SqlSessionTemplate(sqlSessionFactory);
  34.     }
  35. }
复制代码
10. 测试

使用MybatisX生成代码,并且创建com.zbbmeta.controller包下创建TutorialController类,并且在需要切换数据源的方法上使用 @DataSource 注解,切面将根据该注解的配置在方法执行前后进行数据源切换。
  1. @RestController
  2. public class TutorialController {
  3.     @Autowired
  4.     private TutorialService tutorialService;
  5.     @DataSource
  6.     @GetMapping("/list")
  7.     public List<Tutorial> list(){
  8.         return tutorialService.list();
  9.     }
  10.     /**
  11.      *
  12.      * 功能: 在主库创建数据
  13.      * @return {@link Boolean}
  14.      * @author luoheng
  15.      */
  16.     @DataSource(type = DatabaseType.MASTER)
  17.     @GetMapping("/create")
  18.     public Boolean create(){
  19.         Tutorial tutorial = new Tutorial();
  20.         tutorial.setTitle("master");
  21.         tutorial.setDescription("master");
  22.         return tutorialService.save(tutorial);
  23.     }
  24. }
复制代码
使用测试工具,或者浏览器发送请求。
这里由于主,从没有数据同步,直接请求会没有数据。我们可以手动新增一下测试数据
  1. Get请求:http://localhost:8082/list
  2. 返回结果:[{"id":10,"title":"savle","description":"savle","published":1}]
  3. Get请求:http://localhost:8082/create
  4. 返回结果:true
复制代码
这样就可以实现动态数据切换,实现读写分离。但是这个时候会数据库不一致,这里提供一篇文章,来实现主从数据库同步。
实现两个MySQL数据库之间数据同步的方案_两个mysql数据库实时同步-CSDN博客

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

科技颠覆者

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表