系统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
- 引入依赖
<?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>
- 配置文件(注意文件位置,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
- 配置多数据源
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;
}
}
- 添加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();
}
}
- 配置动态数据源
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();
}
}
更多:
-
执行流程:设置标识 → 创建 SqlSession → 动态选择数据源 → 执行 SQL → 清理标识。
-
动态数据源流程原理介绍:
在某个查询中,通过DataSourceContextHolder.setDataSource(“slave”) 设置了当前线程的数据源标识为 “slave”。当执行数据库操作时,DynamicDataSource 会调用 determineCurrentLookupKey() 方法,获取到 “slave”。根据 “slave”,从 TargetDataSources 的 Map 中找到对应的 slaveDataSource,并使用它进行数据库操作。 -
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 '';