一、为什么要做主从复制(简单说)
- 读写分离,扛更高并发写操作走主库,大量查询走从库,MySQL 压力更小、速度更快。
- 数据备份,更安全从库可以随时备份,不影响主库正常使用,主库挂了还有从库。
- 高可用,防止单点故障主库宕机,可以快速切换到从库继续提供服务。
- 缓解主库压力统计、报表、批量查询这类慢查询都放从库,不卡主库。
二、主从复制原理
1. 一句话原理
- 主库(Master) 把所有 "写操作" 记录到 二进制日志 binlog
- 从库(Slave) 拉取 binlog,重放里面的 SQL
- 最终达到主库数据 = 从库数据
2. 三大核心线程
- 主从复制一共就 3 个线程:
- ① 主库:Binlog Dump Thread
- 作用:把 binlog 发给从库
- 一旦从库连上,主库就开这个线程推送日志
- ② 从库:IO Thread
- 作用:去主库拉 binlog
- 拉回来存在本地,叫 中继日志 relay log
- ③ 从库:SQL Thread
- 作用:读 relay log,执行里面的 SQL
- 执行完,从库数据就和主库一致
3. 完整流程(一步不落)
- 主库执行 insert/update/delete/DDL
- 记录到 binlog(日志里是事务 / 行变化)
- 从库 IO 线程连接主库,请求日志
- 主库推送 binlog 给从库
- 从库写入 relay log
- 从库 SQL 线程重放 relay log 里的操作
- 从库数据最终和主库一致
**三、**实现 主库写入 → 从库自动同步。
一、环境说明(mysql:8.0.38)
主库(Master):initial_db
从库(Slave):initial_db
数据库表:user(你提供的表结构)
同步规则:主库写,从库读,主库所有变更自动同步到从库
【第一步:主库服务器执行】
1.创建配置mysql配置文件/opt/mysql/conf/my.cnf
mysqld
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci
init_connect='SET NAMES utf8mb4'
max_connections=1000
server-id=1
log-bin=mysql-bin
binlog_format=ROW
gtid_mode=ON
enforce_gtid_consistency=ON
binlog_do_db=initial_db
binlog_ignore_db=mysql
binlog_ignore_db=information_schema
binlog_ignore_db=performance_schema
binlog_ignore_db=sys
2.修改文件夹权限
chmod 644 /opt/mysql/conf/my.cnf
chown -R 999:999 /opt/mysql
3.docker容器启动mysql命令
docker run -d \
--name mysql8.0.38 \
--restart always \
-p 3306:3306 \
--memory=2g \
--memory-swap=2g \
-e MYSQL_ROOT_PASSWORD=root@1545459747356 \
-e MYSQL_USER=user01 \
-e MYSQL_PASSWORD=user01@151157547356 \
-e MYSQL_DATABASE=initial_db \
-v /opt/mysql/data:/var/lib/mysql \
-v /opt/mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf \
-v /opt/mysql/logs:/var/log/mysql \
mysql:8.0.38
4.创建主从数据库:initial_db
CREATE DATABASE IF NOT EXISTS initial_db;
5.主库创建同步账号
-- 1.先彻底删除旧用户(强制删除)
DROP USER IF EXISTS 'repl'@'%';
-- 2. 刷新权限
FLUSH PRIVILEGES;
-- 3. 重新创建用户,直接指定兼容的认证方式(关键!)
CREATE USER 'repl'@'%' IDENTIFIED WITH mysql_native_password BY 'Repl@123456';
-- 4. 授予主从复制权限
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
--5.查看状态
SHOW MASTER STATUS;
显示结果:

6.创建业务表:
CREATE TABLE `user` (
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`id` bigint NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
【第二步:从库服务器执行】
1.创建配置mysql配置文件/opt/mysql/conf/my.cnf
mysqld
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci
init_connect='SET NAMES utf8mb4'
max_connections=1000
server-id=2
gtid_mode=ON
enforce_gtid_consistency=ON
relay-log=mysql-relay-bin
read_only=1
2.修改文件夹权限
chmod 644 /opt/mysql/conf/my.cnf
chown -R 999:999 /opt/mysql
3.docker容器启动mysql命令
docker run -d \
--name mysql8.0.38 \
--restart always \
-p 3306:3306 \
--memory=2g \
--memory-swap=2g \
-e MYSQL_ROOT_PASSWORD=root@1515665747366 \
-e MYSQL_USER=user01 \
-e MYSQL_PASSWORD=user01@15659747376 \
-e MYSQL_DATABASE=initial_db \
-v /opt/mysql/data:/var/lib/mysql \
-v /opt/mysql/conf/my.cnf:/etc/mysql/conf.d/my.cnf \
-v /opt/mysql/logs:/var/log/mysql \
mysql:8.0.38
4.创建主从数据库:initial_db
CREATE DATABASE IF NOT EXISTS initial_db;
5.从库建立同步
STOP SLAVE;
RESET SLAVE ALL;
RESET MASTER;
CHANGE MASTER TO
MASTER_HOST='116.23.154.176',
MASTER_USER='repl',
MASTER_PASSWORD='Repl@123456',
MASTER_AUTO_POSITION=1;
--开启同步
START SLAVE;
--查看同步数据状态
SHOW SLAVE STATUS;
显示结果如下:
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
结果这样就成功啦:
然后就可以在主库添加数据,数据就会自动同步到从库啦!
四、SpringBoot + MyBatis 实现读写分离
1.原理
1)动态数据源切换(手动实现)
流程:
1.配置 主、从多个数据源
2.实现 AbstractRoutingDataSource 动态路由
3.使用 ThreadLocal 保存当前数据源标识
4.AOP + 自定义注解(@Read/@Write)拦截方法
5.根据注解切换:
查询 → 从库
增删改 / 事务 → 主库
6.MyBatis 执行 SQL 时自动使用当前数据源
** 优点:** 轻量、灵活、无中间件依赖
** 缺点:** 需要自己处理路由、事务、主从切换逻辑
主从延迟主要靠:关键查询强制读主、Redis 缓存兜底、业务兼容来解决。
2.代码实现
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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <!-- 统一使用 3.2.x 稳定版,兼容所有依赖 --> <version>3.2.5</version> <relativePath/> </parent> <groupId>org.chen</groupId> <artifactId>read-write-separation-mybatis</artifactId> <version>0.0.1-SNAPSHOT</version> <name>read-write-separation-mybatis</name> <description>read-write-separation-mybatis</description> <properties> <java.version>17</java.version> </properties> <dependencies> <!-- web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- jdbc + mysql --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- aop --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> <!-- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> </project>
2.配置yml文件
spring: # 主库 datasource: master: jdbc-url: jdbc:mysql://186.63.124.196:3306/initial_db username: root password: root@23213214234 driver-class-name: com.mysql.cj.jdbc.Driver # 从库 slave: jdbc-url: jdbc:mysql://186.12.90.261:3306/initial_db username: root password: root@152323747323 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:**.xml type-aliases-package: org.chen.readwriteseparationmybatis
3.通过ThreadLocal 保存数据源上下文
package org.chen.readwriteseparationmybatis; /** * 数据源上下文(ThreadLocal 保存当前数据源) * */ public class DataSourceContext { private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>(); public static void set(String type) { CONTEXT.set(type); } public static String get() { return CONTEXT.get(); } public static void clear() { CONTEXT.remove(); } }
3.实现 AbstractRoutingDataSource 动态路由
public class DynamicDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey() { return DataSourceContext.get(); } }
package org.chen.readwriteseparationmybatis; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; @Configuration public class DataSourceConfig { // 主库 @Bean @ConfigurationProperties("spring.datasource.master") public DataSource masterDataSource() { return DataSourceBuilder.create().build(); } // 从库 @Bean @ConfigurationProperties("spring.datasource.slave") public DataSource slaveDataSource() { return DataSourceBuilder.create().build(); } // 动态数据源 @Bean @Primary public DataSource dynamicDataSource() { DynamicDataSource dataSource = new DynamicDataSource(); // 设置默认数据源 = 主库 dataSource.setDefaultTargetDataSource(masterDataSource()); // 配置多数据源集合 Map<Object, Object> map = new HashMap<>(); map.put("master", masterDataSource()); map.put("slave", slaveDataSource()); dataSource.setTargetDataSources(map); return dataSource; } }
4.自定义读写注解
package org.chen.readwriteseparationmybatis; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; // 读:从库 @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface Read { }
package org.chen.readwriteseparationmybatis; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; // 写:主库 @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface Write { }
5.通过aop代理实现主从数据源的切换
package org.chen.readwriteseparationmybatis; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; @Aspect @Component @Order(1) // 确保比事务先执行 public class DataSourceAspect { // 拦截 @Read 注解 → 从库 @Before("@annotation(org.chen.readwriteseparationmybatis.Read)") public void read() { DataSourceContext.set("slave"); } // 拦截 @Write 注解 → 主库 @Before("@annotation(org.chen.readwriteseparationmybatis.Write)") public void write() { DataSourceContext.set("master"); } // 方法执行完清空上下文,避免线程复用污染 @After("@annotation(org.chen.readwriteseparationmybatis.Read) || @annotation(org.chen.readwriteseparationmybatis.Write)") public void clear() { DataSourceContext.clear(); } }
6.业务代码实现主从切换
创建user表
CREATE TABLE `user` (
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`id` bigint NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
创建user实体类
package org.chen.readwriteseparationmybatis; public class User { // 构造函数、getter/setter public User() {} public User(Long id, String name) { this.id = id; this.name = name; } private Long id; private String name; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
创建实现类
package org.chen.readwriteseparationmybatis; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class UserService { @Autowired private UserMapper userMapper; // 查询 → 走从库 @Read public List<User> list() { return userMapper.selectAll(); } // 新增/修改 → 走主库 @Write public void add(User user) { userMapper.insert(user); } public void add2(User user) { userMapper.insert(user); } }
创建userMapper
package org.chen.readwriteseparationmybatis; import org.apache.ibatis.annotations.Mapper; import java.util.List; @Mapper public interface UserMapper { List<User> selectAll(); User selectById(Long id); int insert(User user); int update(User user); int deleteById(Long id); }
创建UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.chen.readwriteseparationmybatis.UserMapper"> <!-- 查询所有用户 --> <select id="selectAll" resultType="org.chen.readwriteseparationmybatis.User"> SELECT id, name FROM `user` </select> <!-- 根据 id 查询用户 --> <select id="selectById" parameterType="long" resultType="org.chen.readwriteseparationmybatis.User"> SELECT id, name FROM `user` WHERE id = #{id} </select> <!-- 插入用户(id 由应用层提供) --> <insert id="insert" parameterType="org.chen.readwriteseparationmybatis.User"> INSERT INTO `user` (id, name) VALUES (#{id}, #{name}) </insert> <!-- 根据 id 更新用户姓名 --> <update id="update" parameterType="org.chen.readwriteseparationmybatis.User"> UPDATE `user` SET name = #{name} WHERE id = #{id} </update> <!-- 根据 id 删除用户 --> <delete id="deleteById" parameterType="long"> DELETE FROM `user` WHERE id = #{id} </delete> </mapper>
创建controller类
package org.chen.readwriteseparationmybatis; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; // 新增用户 @PostMapping public String addUser(@RequestBody User user) { userService.add(user); return"添加成功" ; } // 查询所有用户 @GetMapping public List<User> getAllUsers() { return userService.list(); } @PostMapping("/add2") public String addUser2(@RequestBody User user) { userService.add2(user); return"添加成功" ; } }
最后调用接口localhost:8080/api/users就🆗啦,然后就可以看到数据保存到主库,然后查询的数据是从库。