mysql主从复制搭建与springboot项目整合测试

系统ubuntu

主库配置 /etc/mysql/mysql.conf.d/mysqld.cnf

[mysqld]
server-id = 1
log_bin = mysql-bin
binlog_do_db = mytest # 需要同步的数据库

重启

sudo systemctl restart mysql

进入mysql创建用于复制的账号 repl,repl

CREATE USER 'repl'@'%' IDENTIFIED WITH mysql_native_password BY 'repl';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;

查看主库状态

SHOW MASTER STATUS;

记录输出的File(如mysql-bin.000001)和Position(如154)。
在这里插入图片描述

我发现主库重启后, mysql-bin.000003 也就是binlog文件会改变,需要引入组件,不然从库每次都要重新配置文件。
解决方案使用GTID(全局事务标识)

GTID(Global Transaction Identifier)是MySQL 5.6引入的特性,可以自动跟踪主库的事务,无需手动指定日志文件和位置。

步骤
在主库上启用GTID
编辑主库的配置文件:/etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
gtid_mode=ON
enforce_gtid_consistency=ON
重启MySQL服务:
sudo systemctl restart mysql

在从库上启用GTID
编辑从库的/etc/mysql/mysql.conf.d/mysqld.cnf文件:
[mysqld]
gtid_mode=ON
enforce_gtid_consistency=ON
重启MySQL服务:
sudo systemctl restart mysql

重新配置主从复制
在从库上重新配置主库信息,使用GTID:
CHANGE MASTER TO
MASTER_HOST='主库IP',
MASTER_USER='repl',
MASTER_PASSWORD='repl',
MASTER_AUTO_POSITION=1;

START REPLICA;

优点: 无需手动指定日志文件和位置。主库重启后,从库会自动从正确的位置继续复制。

要确认GTID(全局事务标识)是否成功启用,可以通过以下步骤进行检查:

1. 检查GTID模式状态
在MySQL命令行中执行以下命令,查看GTID模式是否已启用:
SHOW VARIABLES LIKE 'gtid_mode';
SHOW VARIABLES LIKE 'enforce_gtid_consistency';
预期输出
gtid_mode 应为 ON。
enforce_gtid_consistency 应为 ON。
如果输出为 OFF,说明GTID未启用。

2. 检查主库的GTID状态
在主库上执行以下命令,查看GTID的执行情况:
SHOW MASTER STATUS;
预期输出
Executed_Gtid_Set 字段应显示已执行的GTID集合,例如:
Executed_Gtid_Set: 3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5
这表示GTID已启用,并且主库已经执行了某些事务(需要有事务提交才会有Executed_Gtid_Set字段,也就是建表语句或者增删改查了数据库且提交事务)。
3. 检查从库的GTID状态
在从库上执行以下命令,查看GTID的复制状态:
SHOW REPLICA STATUS\G;
Retrieved_Gtid_Set:从主库获取的GTID集合。
Executed_Gtid_Set:从库已执行的GTID集合。
Auto_Position:是否启用了GTID自动定位(应为 1)。
示例输出
Retrieved_Gtid_Set: 3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5
Executed_Gtid_Set: 3E11FA47-71CA-11E1-9E33-C80AA9429562:1-5
Auto_Position: 1

手动在主库建表,以及插入数据测试一下,同步成功。

整合springboot

  1. 引入依赖
 	<?xml version="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>

    <groupId>org.example</groupId>
    <artifactId>MyShow</artifactId>
    <version>1.0-SNAPSHOT</version>


    <!-- 父项目 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>


    <dependencies>
        <!-- Spring Boot Starter 依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>


        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>


        <!-- 连接池 -->
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
        </dependency>

        <!-- MySQL 驱动 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.23</version>
        </dependency>

        <!-- AOP 支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!-- MyBatis Spring Boot Starter -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

        <!-- Spring Boot Starter Data JPA for database access -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

    </dependencies>


    <!-- 构建配置 -->
    <build>
        <plugins>
            <!-- Spring Boot Maven 插件 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>
  1. 配置文件(注意文件位置,spring默认仅加载resourse下)
server:
  port: 8081

spring:
  #  项目名称
  application:
    name: myshow
  #  模板引擎配置
  thymeleaf:
    encoding: UTF-8
    cache: false
    suffix: .html
    prefix: classpath:templates/
    
# 配置主库与从库
  datasource:
    master:
      url: jdbc:mysql://192.168.176.128:3306/mytest?useSSL=false&allowPublicKeyRetrieval=true
      username: zzz
      password: zzz
      driver-class-name: com.mysql.cj.jdbc.Driver
    slave:
      url: jdbc:mysql://192.168.176.129:3306/mytest?useSSL=false&allowPublicKeyRetrieval=true
      username: zzz
      password: zzz
      driver-class-name: com.mysql.cj.jdbc.Driver
  hikari:
    maximum-pool-size: 10

logging:
  level:
    org.springframework.jdbc: DEBUG
    com.mysql.cj.jdbc: DEBUG

  1. 配置多数据源
package com.myshow.config;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

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

@Configuration
public class DataSourceConfig {

    @Value("${spring.datasource.master.url}")
    private String masterUrl;
    @Value("${spring.datasource.master.username}")
    private String masterName;
    @Value("${spring.datasource.master.password}")
    private String masterPasswd;

    @Value("${spring.datasource.slave.url}")
    private String slaveUrl;
    @Value("${spring.datasource.slave.username}")
    private String slaveName;
    @Value("${spring.datasource.slave.password}")
    private String slavePasswd;

// 主库DataSource
    @Bean(name = "masterDataSource")
    public DataSource masterDataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setUrl(masterUrl);
        dataSource.setUsername(masterName);
        dataSource.setPassword(masterPasswd);
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        return dataSource;
    }

// 从库DataSource
    @Bean(name = "slaveDataSource")
    public DataSource slaveDataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setUrl(slaveUrl);
        dataSource.setUsername(slaveName);
        dataSource.setPassword(slavePasswd);
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        return dataSource;
    }

// 创建自定义动态数据源,并装配主从数据源,动态数据源类有了两个属性(主,从数据源)
    @Bean
    public DataSource dynamicDataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slaveDataSource") DataSource slaveDataSource) {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("master", masterDataSource);
        dataSourceMap.put("slave", slaveDataSource);
        dynamicDataSource.setDefaultTargetDataSource(masterDataSource); // 默认数据源
        dynamicDataSource.setTargetDataSources(dataSourceMap);
        return dynamicDataSource;
    }
}

  1. 添加DataSource全局上下文 用于切库
package com.myshow.config;

public class DataSourceContextHolder {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    public static void setMaster() {
        contextHolder.set("master");
    }

    public static void setSlave() {
        contextHolder.set("slave");
    }

    public static String getDataSource() {
        return contextHolder.get();
    }

    public static void clear() {
        contextHolder.remove();
    }
}

  1. 配置动态数据源
package com.myshow.config;

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

public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
    // 根据当前上下文动态切换数据源(获取master与slave字符串)
        return DataSourceContextHolder.getDataSource();
    }
}

6.配置mybatis

package com.myshow.config;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

@Configuration
public class MyBatisConfig {

// 创建SqlSessionFactory,装配dynamicDataSource
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dynamicDataSource) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        // 绑定数据源用于获取数据库连接
        sessionFactory.setDataSource(dynamicDataSource);
        sessionFactory.setMapperLocations(
            new PathMatchingResourcePatternResolver()
                .getResources("classpath:com/myshow/*.xml") // MyBatis XML 文件路径
        );
        return sessionFactory.getObject();
    }
}

7.添加注解,利用注解加AOP方法自动设置数据源为从库,不用手动设置DataSource

package com.myshow.config;

import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ReadOnly {
}

8.配置aop

package com.myshow.config;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ReadOnlyAspect {

    // 拦截所有标记 @ReadOnly 的方法
    @Around("@annotation(com.myshow.config.ReadOnly)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            DataSourceContextHolder.setSlave(); // 切换到从库
            return joinPoint.proceed();
        } finally {
       		 // 需要清空数据源上下文里的值
            DataSourceContextHolder.clear();
        }
    }
}

9.添加事务管理器

package com.myshow.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Configuration
public class TransactionManagerConfig {

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dynamicDataSource) {
        return new DataSourceTransactionManager(dynamicDataSource);
    }
}

用法介绍:
默认是操作主表(增删改场景),
仅需要查询的方法需要加上注解@ReadOnly即可

package com.myshow.service;

import com.myshow.config.ReadOnly;
import com.myshow.entity.Product;
import com.myshow.mapper.ProductMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ProductService {

    @Autowired
    private ProductMapper productMapper;

    public String save(Product product) {
        productMapper.insertProduct(product);
        return "ok";
    }

    // 读操作(自动切到从库)
    @ReadOnly
    public List<Product> getProducts() {
        return productMapper.getProducts();
    }

}

更多:

  1. 执行流程:设置标识 → 创建 SqlSession → 动态选择数据源 → 执行 SQL → 清理标识。

  2. 动态数据源流程原理介绍:
    在某个查询中,通过DataSourceContextHolder.setDataSource(“slave”) 设置了当前线程的数据源标识为 “slave”。当执行数据库操作时,DynamicDataSource 会调用 determineCurrentLookupKey() 方法,获取到 “slave”。根据 “slave”,从 TargetDataSources 的 Map 中找到对应的 slaveDataSource,并使用它进行数据库操作。

  3. SqlSessionFactory 的作用:
    SqlSessionFactory 是 MyBatis 的核心工厂类,负责创建 SqlSession 对象。它的主要作用包括:
    管理数据源:SqlSessionFactory 需要绑定一个数据源(DataSource),用于获取数据库连接。
    加载 MyBatis 配置:包括 XML 映射文件、类型别名、插件等。
    创建 SqlSession:SqlSession 是 MyBatis 执行 SQL 的入口,每次数据库操作都需要通过 SqlSession 完成。

源码解析:AbstractRoutingDataSource.determineTargetDataSource()

protected DataSource determineTargetDataSource() {
    // 1. 调用 determineCurrentLookupKey() 获取当前数据源标识
    Object lookupKey = determineCurrentLookupKey();
    
    // 2. 根据标识从 resolvedDataSources 中获取对应的数据源
    DataSource dataSource = this.resolvedDataSources.get(lookupKey);
    
    // 3. 如果未找到数据源且允许回退到默认数据源,则使用默认数据源
    if (dataSource == null && (this.lenientFallback || lookupKey == null)) {
        dataSource = this.resolvedDefaultDataSource;
    }
    
    // 4. 如果仍未找到数据源,抛出异常
    if (dataSource == null) {
        throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");
    }
    
    return dataSource;
}

停止io复制程才能修改数据库主从配置

STOP REPLICA IO_THREAD FOR CHANNEL '';
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值