MySql的主从复制

数据库读写分离对于大型系统或者访问量很高的互联网应用来说,是必不可少的一个重要功能。Insert SQL就是几十个毫秒,Select SQL则要几秒到几分钟才能有结果,很多复杂的SQL,其消耗服务器CPU的能力超强,不亚于死循环的威力。高峰时段的一些复杂SQL查询就导致数据库服务器CPU爆表,系统陷入瘫痪,严重情况下可能导致数据库崩溃。我们应该尽量避免没有主从复制机制的单节点数据库。

对于MySQL来说,标准的读写分离是主从模式,一个写节点Master后面跟着多个读节点,读节点的数量取决于系统的压力,通常是1-3个读节点的配置。

MySQL主从复制的原理

1.主库上记录二进制日志(稍后介绍如何设置)。在每次准备提交事务完成数据更新前,主库将数据更新的事件记录到二进制日志中,MySQL会按事务提交的顺序而非每条语句的执行顺序来记录二进制日志。

2.备库将主库的二进制日志复制到其本地的中继日志中,备库会启动一个工作线程,称为IO线程,I/O线程跟主库建立一个普通的客户端连接,然后在主库上启动一个特殊的二进制转储(binhg dump、线程该线程没有对应的SQL命令),这个二进制转储线程会读取主库上二进制日志中的事件

3.备库的SQL线程执行最后一步,该线程从中继日志中读取事件并在备库执行,从而实现备库数据的更新主从复制是mysql来实现,mycat配合mysql的主从来完成功能。

环境搭建

拉取镜像

docker pull mysql

创建两个文件夹用于存储主机和从机的数据

D:\mysql\master\data
D:\mysql\slave\data

注:此教程是基于docker desktop环境搭建的,读者也可以通过Linux虚拟机搭建

主机配置

创建主机容器

docker run -d ^
--name mysql-master ^
-p 3307:3306 ^
-e MYSQL_ROOT_PASSWORD=123456 ^
-e TZ=Asia/Shanghai ^
-v D:\mysql\master\data:/var/lib/mysql ^
--restart unless-stopped ^
mysql:8.0 ^
--server-id=1 ^
--log-bin=mysql-bin ^
--binlog-format=ROW ^
--character-set-server=utf8mb4 ^
--collation-server=utf8mb4_general_ci ^
--lower_case_table_names=1

Alt
进入主库容器

docker exec -it mysql-master bash
# 登录mysql
mysql -uroot -p123456

Alt
创建主从同步专用账号

# 创建同步用户 slave,允许所有IP访问
CREATE USER 'slave'@'%' IDENTIFIED WITH mysql_native_password BY '123456';
# 授予复制权限
GRANT REPLICATION SLAVE ON *.* TO 'slave'@'%';
# 刷新权限
FLUSH PRIVILEGES;

Alt
查看主库状态(关键!记录参数)

SHOW MASTER STATUS;

一定要抄下:文件名 File、偏移 Position,后面从库要用
Alt

从机配置

创建从机容器

docker run -d ^
--name mysql-slave ^
-p 3308:3306 ^
-e MYSQL_ROOT_PASSWORD=123456 ^
-e TZ=Asia/Shanghai ^
-v D:\mysql\slave\data:/var/lib/mysql ^
--restart unless-stopped ^
mysql:8.0 ^
--server-id=2 ^
--relay-log=relay-bin ^
--log-bin=mysql-bin ^
--read-only=1 ^
--character-set-server=utf8mb4 ^
--collation-server=utf8mb4_general_ci ^
--lower_case_table_names=1

Alt
进入从库

docker exec -it mysql-slave bash
# 登录数据库
mysql -uroot -p123456

Alt
先停掉同步

STOP SLAVE;
RESET SLAVE ALL;

Alt
绑定主库(把 File 和 Position 换成你刚才看到的)

CHANGE REPLICATION SOURCE TO
SOURCE_HOST='host.docker.internal',
SOURCE_PORT=3307,
SOURCE_USER='slave',
SOURCE_PASSWORD='123456',
SOURCE_LOG_FILE='mysql-bin.000003',
SOURCE_LOG_POS=860;

Alt
启动同步

START SLAVE;

查看状态

SHOW SLAVE STATUS\G;

关键成功标志解读
在这里插入图片描述
看到这两个 = 主从成功

Slave_IO_Running: Yes
Slave_SQL_Running: Yes

Alt
如果不成功,请删除master、slave两个容器,重新搭建

docker rm -f mysql-master mysql-slave
测试
  1. 主库插入测试数据
-- 创建测试库和表
CREATE DATABASE test_sync_db;
USE test_sync_db;
CREATE TABLE user_info (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(20),
  create_time DATETIME DEFAULT NOW()
);

-- 插入数据
INSERT INTO user_info (id, name) VALUES (1, '测试主从同步'), (2, 'MySQL 8.0');
  1. 从库验证数据
-- 查看同步的数据
USE test_sync_db;
SELECT * FROM user_info;

预期输出(和主库完全一致):

+----+------------------+---------------------+
| id | name             | create_time         |
+----+------------------+---------------------+
|  1 | 测试主从同步      | 2026-03-31 21:xx:xx |
|  2 | MySQL 8.0        | 2026-03-31 21:xx:xx |
+----+------------------+---------------------+

基于SpirngBoot、Mybatis-Plus的读写分离实现

核心步骤

SpringBoot 3.5.7 结合 MyBatis-Plus 实现 MySQL 主从读写分离的核心思路是基于动态数据源切换 + AOP 切面拦截,将写操作(新增、修改、删除)路由到主库,读操作(查询)路由到从库,充分利用 MySQL 主从复制的特性提升系统读写性能,具体实现逻辑可分为以下几个核心步骤:

1. 基础环境与依赖配置

首先搭建适配的技术环境:

  • 引入 SpringBoot 3.5.7 核心依赖、MyBatis-Plus 3.5.8+(适配 Spring6.2 底层 API)、Druid 数据源(适配 SpringBoot 3)及 AOP 相关依赖,解决版本兼容性问题;
  • 在配置文件(application.yml)中分别配置主库(写)和从库(读)的连接信息(URL、用户名、密码、驱动类),明确区分两类数据源的配置前缀。
2. 动态数据源核心实现

基于 Spring 提供的 AbstractRoutingDataSource 抽象类实现动态数据源切换,这是读写分离的核心底层支撑:

  • 定义数据源类型枚举(MASTER/SLAVE),标识主库和从库;
  • 实现 DynamicDataSource 类继承 AbstractRoutingDataSource,重写 determineCurrentLookupKey() 方法,该方法会从线程上下文(ThreadLocal)中获取当前需要使用的数据源类型,作为数据源切换的核心依据;
  • 构建 DataSourceContextHolder 工具类,通过 ThreadLocal 存储和管理当前线程的数据源类型(MASTER/SLAVE),保证多线程环境下数据源切换的线程安全,且在操作完成后清除 ThreadLocal 中的数据,避免内存泄漏。
3. 数据源配置与注入

将主从数据源注入 Spring 容器,并整合为动态数据源:

  • 分别配置主库(masterDataSource)和从库(slaveDataSource)的 Druid 数据源 Bean,绑定配置文件中的连接信息;
  • 构建动态数据源 Bean(dynamicDataSource),将主从数据源存入 targetDataSources 映射表,指定主库为默认数据源(保证未明确指定时默认走写库);
  • 配置事务管理器和 MyBatis-Plus 的 SqlSessionFactory,指定动态数据源为底层数据源,确保 MyBatis-Plus 操作基于动态数据源执行。
4. AOP 切面实现读写路由拦截

通过自定义注解 + AOP 切面,实现业务方法层面的数据源自动切换:

  • 定义自定义注解 @ReadWriteSplit,可指定注解值为 MASTER/SLAVE,用于标记方法对应的数据源类型;
  • 编写 AOP 切面类 ReadWriteSplitAspect,以该注解为切入点,在方法执行前拦截并解析注解中的数据源类型,通过 DataSourceContextHolder 设置当前线程的数据源类型;
  • 切面执行优先级高于事务切面,保证数据源切换先于事务生效;方法执行完成后(finally 块)清除线程上下文的数据源类型,避免线程复用导致的数据源错乱。
5. 业务层适配与验证

在业务代码中落地读写分离规则:

  • 对新增、修改、删除等写操作方法,标记 @ReadWriteSplit(MASTER)(或默认使用主库),确保写操作路由到主库,保证数据一致性;
  • 对查询类读操作方法,标记 @ReadWriteSplit(SLAVE),将读请求路由到从库,分担主库压力;
  • 测试验证:写操作后数据同步到从库,读操作能从从库获取最新数据,且日志可清晰看到数据源切换过程,最终实现「写主库、读从库」的读写分离效果。

核心设计原则

  • 线程安全:通过 ThreadLocal 隔离线程的数据源上下文,避免多线程环境下的数据源冲突;
  • 兼容性:适配 SpringBoot 3.5.7 与 MyBatis-Plus 的版本兼容,手动配置 MyBatis-Plus 专属的 MybatisSqlSessionFactoryBean 避免类型解析错误;
  • 可扩展:若需扩展多从库,可在 DataSourceContextHolder 中增加负载均衡逻辑(如轮询、随机),无需修改核心切换逻辑;
  • 事务适配:事务内的所有操作强制走主库,避免读未提交的数据,保证事务一致性。

读写时序图

写时序图
MySQL主库 主库数据源 DynamicDataSource DataSourceContextHolder 读写分离切面 Service层 Controller层 客户端 MySQL主库 主库数据源 DynamicDataSource DataSourceContextHolder 读写分离切面 Service层 Controller层 客户端 写操作(新增/修改/删除)- 路由到主库 发起写操作请求(如POST /user/add) 调用标记@ReadWriteSplit(MASTER)的方法 触发切面拦截 设置数据源类型为MASTER 数据源类型设置完成 执行业务方法 获取数据库连接 获取当前数据源类型(MASTER) 返回MASTER 路由到主库数据源 执行写SQL(INSERT/UPDATE/DELETE) 返回操作结果 返回数据库连接结果 返回SQL执行结果 返回业务处理结果 返回响应(如"新增成功") 清除线程内数据源类型(finally) 清除完成
读时序图
MySQL从库 从库数据源 DynamicDataSource DataSourceContextHolder 读写分离切面 Service层 Controller层 客户端 MySQL从库 从库数据源 DynamicDataSource DataSourceContextHolder 读写分离切面 Service层 Controller层 客户端 读操作(查询)- 路由到从库 发起读操作请求(如GET /user/1) 调用标记@ReadWriteSplit(SLAVE)的方法 触发切面拦截 设置数据源类型为SLAVE 数据源类型设置完成 执行业务方法 获取数据库连接 获取当前数据源类型(SLAVE) 返回SLAVE 路由到从库数据源 执行读SQL(SELECT) 返回查询结果 返回数据库连接结果 返回SQL执行结果 返回业务处理结果 返回响应(如用户数据) 清除线程内数据源类型(finally) 清除完成

实现代码

代码仓库:https://gitee.com/xiangweilll/mysql-master-slave-replication.git

项目结构
Alt

核心依赖
<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>

  <groupId>com.elvis</groupId>
  <artifactId>mysql-master-slave-replication</artifactId>
  <version>1.0</version>
  <packaging>jar</packaging>

  <name>mysql-master-slave-replication</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <spring-boot.version>3.5.7</spring-boot.version>
  </properties>

  <dependencies>
    <!-- SpringBoot核心 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- MySQL驱动 -->
    <dependency>
      <groupId>com.mysql</groupId>
      <artifactId>mysql-connector-j</artifactId>
      <scope>runtime</scope>
    </dependency>

    <!-- MyBatis-Plus -->
    <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
      <version>3.5.12</version>
    </dependency>
    <!-- 数据源驱动 -->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid-spring-boot-3-starter</artifactId>
      <version>1.2.20</version>
    </dependency>

    <!-- Lombok(简化代码) -->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>

    <!-- AOP -->
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
    </dependency>

    <!-- 测试 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>${spring-boot.version}</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <source>17</source>
          <target>17</target>
          <encoding>UTF-8</encoding>
          <compilerArgs>
            <arg>-parameters</arg>  <!--在编译代码是保留参数的名字-->
          </compilerArgs>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>${spring-boot.version}</version>
        <configuration>
          <mainClass>com.woniuxy.LangchainApplication</mainClass>
          <skip>true</skip>
        </configuration>
        <executions>
          <execution>
            <id>repackage</id>
            <goals>
              <goal>repackage</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>
主配置文件
spring:
  # 数据源配置
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    # 主库(写)
    master:
      url: jdbc:mysql://localhost:3307/test_sync_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver
    # 从库(读)
    slave:
      url: jdbc:mysql://localhost:3308/test_sync_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
      username: root
      password: 123456
      driver-class-name: com.mysql.cj.jdbc.Driver

# MyBatis-Plus配置
mybatis-plus:
  mapper-locations: classpath:mapper/**/*.xml
  type-aliases-package: com.example.entity
  configuration:
    map-underscore-to-camel-case: true # 下划线转驼峰
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL日志(调试用)
数据源枚举

配置主从库连接信息(对应搭建的 3307 主库、3308 从库):

package com.elvis.enums;

/**
 * 数据源类型枚举
 */
public enum DataSourceType {
    /**
     * 主库(写操作)
     */
    MASTER,
    /**
     * 从库(读操作)
     */
    SLAVE
}
数据源上下文

用 ThreadLocal 存储当前线程的数据源类型:

package com.elvis.config;

import com.elvis.enums.DataSourceType;

/**
 * 数据源上下文持有者(线程隔离)
 */
public class DataSourceContextHolder {

    /**
     * 线程本地存储数据源类型
     */
    private static final ThreadLocal<DataSourceType> CONTEXT_HOLDER = new ThreadLocal<>();

    /**
     * 设置数据源类型
     */
    public static void setDataSourceType(DataSourceType type) {
        CONTEXT_HOLDER.set(type);
    }

    /**
     * 获取当前数据源类型
     */
    public static DataSourceType getDataSourceType() {
        // 默认使用主库
        return CONTEXT_HOLDER.get() == null ? DataSourceType.MASTER : CONTEXT_HOLDER.get();
    }

    /**
     * 清除数据源类型
     */
    public static void clearDataSourceType() {
        CONTEXT_HOLDER.remove();
    }
}
动态数据源

核心类,实现数据源动态切换:

package com.elvis.config;

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 动态数据源(根据上下文切换主从库)
 */
public class DynamicDataSource extends AbstractRoutingDataSource {

    /**
     * 核心方法:返回当前线程对应的数据源key
     */
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getDataSourceType();
    }
}
数据源配置

配置主从数据源并注入动态数据源:

package com.elvis.config;

import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceBuilder;
import com.elvis.enums.DataSourceType;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * 数据源配置类
 */
@Configuration
public class DataSourceConfig {

    /**
     * 配置主库数据源
     */
    @Bean(name = "masterDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.master")
    public DataSource masterDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * 配置从库数据源
     */
    @Bean(name = "slaveDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.slave")
    public DataSource slaveDataSource() {
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * 配置动态数据源(主库为默认)
     */
    @Bean(name = "dynamicDataSource")
    @Primary // 优先使用该数据源
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();

        // 配置主从数据源
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceType.MASTER, masterDataSource());
        targetDataSources.put(DataSourceType.SLAVE, slaveDataSource());

        // 设置数据源映射
        dynamicDataSource.setTargetDataSources(targetDataSources);
        // 设置默认数据源(主库)
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource());

        return dynamicDataSource;
    }

    /**
     * 配置事务管理器
     */
    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dynamicDataSource());
    }
}
读写分离注解

自定义注解,标记方法使用主库 / 从库:

package com.elvis.annotation;

import com.elvis.enums.DataSourceType;

import java.lang.annotation.*;

/**
 * 读写分离注解
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ReadWriteSplit {

    /**
     * 数据源类型(默认主库)
     */
    DataSourceType value() default DataSourceType.MASTER;
}
切面实现

通过 AOP 拦截注解,切换数据源:

package com.elvis.aspect;

import com.elvis.annotation.ReadWriteSplit;
import com.elvis.config.DataSourceContextHolder;
import com.elvis.enums.DataSourceType;
import lombok.extern.slf4j.Slf4j;
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.springframework.core.Ordered;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 读写分离切面(拦截注解,切换数据源)
 */
@Slf4j
@Aspect
@Component
public class ReadWriteSplitAspect implements Ordered {

    /**
     * 切入点:拦截所有带@ReadWriteSplit注解的方法
     */
    @Pointcut("@annotation(com.elvis.annotation.ReadWriteSplit)")
    public void readWriteSplitPoint() {
    }

    /**
     * 环绕通知:切换数据源
     */
    @Around("readWriteSplitPoint()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取方法上的注解
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        ReadWriteSplit annotation = method.getAnnotation(ReadWriteSplit.class);

        // 设置数据源类型
        DataSourceType dataSourceType = annotation.value();
        DataSourceContextHolder.setDataSourceType(dataSourceType);
        log.info("切换数据源:{}", dataSourceType.name());

        try {
            // 执行方法
            return joinPoint.proceed();
        } finally {
            // 清除数据源类型(避免线程复用导致问题)
            DataSourceContextHolder.clearDataSourceType();
            log.info("清除数据源类型");
        }
    }

    /**
     * 设置切面优先级(高于事务切面)
     */
    @Override
    public int getOrder() {
        return 1;
    }
}
实体类

对应 user_info 表:

package com.elvis.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@TableName("user_info")
public class UserInfo {

    @TableId(type = IdType.AUTO)
    private Integer id;

    private String name;

    //指定json格式
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
}
Mapper接口
package com.elvis.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.elvis.entity.UserInfo;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserInfoMapper extends BaseMapper<UserInfo> {
}
Service层
package com.elvis.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.elvis.entity.UserInfo;

public interface UserInfoService extends IService<UserInfo> {

    /**
     * 新增用户(写操作,主库)
     */
    boolean addUser(UserInfo userInfo);

    /**
     * 查询用户(读操作,从库)
     */
    UserInfo getUserById(Integer id);
}

实现类

package com.elvis.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.elvis.annotation.ReadWriteSplit;
import com.elvis.entity.UserInfo;
import com.elvis.enums.DataSourceType;
import com.elvis.mapper.UserInfoMapper;
import com.elvis.service.UserInfoService;
import org.springframework.stereotype.Service;

@Service
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {

    /**
     * 写操作:使用主库(默认,也可显式指定)
     */
    @Override
    @ReadWriteSplit(DataSourceType.MASTER)
    public boolean addUser(UserInfo userInfo) {
        return save(userInfo);
    }

    /**
     * 读操作:使用从库
     */
    @Override
    @ReadWriteSplit(DataSourceType.SLAVE)
    public UserInfo getUserById(Integer id) {
        return getById(id);
    }
}
Controller 层
package com.elvis.controller;

import com.elvis.entity.UserInfo;
import com.elvis.service.UserInfoService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
@RequiredArgsConstructor
public class UserInfoController {

    private final UserInfoService userInfoService;

    /**
     * 新增用户(写操作→主库)
     */
    @PostMapping("/add")
    public String addUser(@RequestBody UserInfo userInfo) {
        boolean success = userInfoService.addUser(userInfo);
        return success ? "新增成功" : "新增失败";
    }

    /**
     * 查询用户(读操作→从库)
     */
    @GetMapping("/{id}")
    public UserInfo getUser(@PathVariable Integer id) {
        return userInfoService.getUserById(id);
    }
}
启动类
package com.elvis;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

/**
 * @Author XiangWei
 * @Date 2026/4/1 14:17
 * @Description: 启动类(排除默认数据源自动配置)
 */
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) // 排除默认数据源配置
@MapperScan("com.elvis.mapper") // 扫描Mapper接口
@EnableAspectJAutoProxy(exposeProxy = true) // 开启AOP

public class MasterSlaveReplicationApplication {
    public static void main(String[] args) {
        // 启动Spring Boot应用//
        SpringApplication.run(MasterSlaveReplicationApplication.class, args);
    }
}
测试

利用postman、apifox等接口测试工具测试添加用户和查询接口,查看控制台运行日志

添加

2026-04-01T14:52:45.071+08:00  INFO 39900 --- [nio-8080-exec-6] com.elvis.aspect.ReadWriteSplitAspect    : 切换数据源:MASTER
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2b367182] was not registered for synchronization because synchronization is not active
2026-04-01T14:52:45.100+08:00  INFO 39900 --- [nio-8080-exec-6] com.alibaba.druid.pool.DruidDataSource   : {dataSource-2} inited
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@325d26f0] will not be managed by Spring
==>  Preparing: INSERT INTO user_info ( name ) VALUES ( ? )
==> Parameters: xiang(String)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2b367182]
2026-04-01T14:52:45.121+08:00  INFO 39900 --- [nio-8080-exec-6] com.elvis.aspect.ReadWriteSplitAspect    : 清除数据源类型

查询

2026-04-01T14:31:18.436+08:00  INFO 39900 --- [nio-8080-exec-4] com.elvis.aspect.ReadWriteSplitAspect    : 切换数据源:SLAVE
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@783d5eba] was not registered for synchronization because synchronization is not active
JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@112d95ea] will not be managed by Spring
==>  Preparing: SELECT id,name,create_time FROM user_info WHERE id=?
==> Parameters: 3(Integer)
<==    Columns: id, name, create_time
<==        Row: 3, elvis, 2026-04-01 14:29:31
<==      Total: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@783d5eba]
2026-04-01T14:31:18.439+08:00  INFO 39900 --- [nio-8080-exec-4] com.elvis.aspect.ReadWriteSplitAspect    : 清除数据源类型
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐