mysql实现主从复制以及springboot实现读写分离

一、为什么要做主从复制(简单说)

  1. 读写分离,扛更高并发写操作走主库,大量查询走从库,MySQL 压力更小、速度更快。
  2. 数据备份,更安全从库可以随时备份,不影响主库正常使用,主库挂了还有从库。
  3. 高可用,防止单点故障主库宕机,可以快速切换到从库继续提供服务。
  4. 缓解主库压力统计、报表、批量查询这类慢查询都放从库,不卡主库。

二、主从复制原理

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就🆗啦,然后就可以看到数据保存到主库,然后查询的数据是从库。

相关推荐
山川行2 小时前
Python快速闯关专栏的总结
java·开发语言·笔记·python·算法·visual studio code·visual studio
两年半的个人练习生^_^2 小时前
如何自己实现多数据源
数据库
AI周红伟2 小时前
周红伟:关于OpenClaw安全使用提醒
大数据·数据库·人工智能·安全·腾讯云·openclaw
默归2 小时前
Java云原生时代面临的挑战与变革
java·开发语言·云原生
斯密码赛我是美女2 小时前
周报--2
android·数据库
marsh02062 小时前
23 openclaw防止SQL注入:参数化查询与ORM安全使用
数据库·sql·安全·ai·编程·技术
原来是猿2 小时前
为什么要配置环境变量?
linux·数据库·python
星辰_mya2 小时前
MVCC 与事务隔离:MySQL 如何实现“读不阻塞写”?
java·数据库·mysql·面试·架构