MySQL的主从同步及读写分离

随着互联网、移动应用和物联网等领域的迅猛发展,数据量呈现出爆炸式增长,对数据库的性能和扩展性提出了更高的要求。MySQL 作为数据库领域的中流砥柱,不断进化以适应这些变化。主从同步及读写分离作为 MySQL 应对高并发和大数据挑战的关键技术,已经成为众多企业构建高性能数据库架构的标配。了解和掌握这两项技术,不仅能让你紧跟数据库技术发展的潮流,还能为你的项目和业务带来显著的性能提升和成本优化。下面,我们就一同走进 MySQL 主从同步及读写分离的世界。

一、使用虚拟机搭建三台MySQL服务

由于我已经提前搭建好一台环境,所以直接跳过搭建,如果没有搭建的话可以参考此篇文章:

在VMware中安装CentOS7(超详细的图文教程)_在vmware上安装centos-CSDN博客

右键点击管理-》克隆

克隆完成之后登录虚拟机

当你在虚拟机中完成克隆操作后,新克隆的服务器通常会继承原虚拟机的 IP 地址,为了避免 IP 冲突,你需要对新克隆服务器的 IP 地址进行修改。不同的操作系统,修改 IP 地址的方式也有所不同,以下为你分别介绍常见操作系统的修改方法:

CentOS 7 及以上版本

1. 查看网络配置文件

网络配置文件通常存放在 /etc/sysconfig/network-scripts/ 目录下,以 ifcfg- 开头,后面跟着网络接口名(如 eth0ens33 等)。使用以下命令查看文件:

收起

bash

ls /etc/sysconfig/network-scripts/
2. 编辑网络配置文件

使用文本编辑器(如 vi 或 nano)打开对应的网络配置文件,例如 ifcfg-ens33

收起

bash

vi /etc/sysconfig/network-scripts/ifcfg-ens33
3. 修改配置参数

将文件中的 IP 地址、子网掩码、网关等信息修改为你需要的值。示例如下:

收起

plaintext

TYPE=Ethernet
PROXY_METHOD=none
BROWSER_ONLY=no
BOOTPROTO=static  # 使用静态 IP 地址
DEFROUTE=yes
IPV4_FAILURE_FATAL=no
IPV6INIT=yes
IPV6_AUTOCONF=yes
IPV6_DEFROUTE=yes
IPV6_FAILURE_FATAL=no
IPV6_ADDR_GEN_MODE=stable-privacy
NAME=ens33
UUID=xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx  # 可保留原 UUID 或重新生成
DEVICE=ens33
ONBOOT=yes  # 开机自动启用网络接口
IPADDR=192.168.1.100  # 修改为新的 IP 地址
NETMASK=255.255.255.0  # 修改为对应的子网掩码
GATEWAY=192.168.1.1  # 修改为对应的网关地址
DNS1=8.8.8.8  # 修改为对应的 DNS 服务器地址
4. 保存并退出文件

在 vi 编辑器中,按下 Esc 键,输入 :wq 并回车保存退出。

5. 重启网络服务

使用以下命令重启网络服务使配置生效:

收起

bash

systemctl restart network

 通过docker下载MySQL镜像

sudo docker pull mysql:5.7

如果遇到拉取失败可以尝试下面的方法
第一步更改docker配置:

sudo vi /etc/docker/daemon.json

第二步:

{

    "registry-mirrors": [

    "https://2a6bf1988cb6428c877f723ec7530dbc.mirror.swr.myhuaweicloud.com",

    "https://docker.m.daocloud.io",

    "https://hub-mirror.c.163.com",

    "https://mirror.baidubce.com",

    "https://your_preferred_mirror",

    "https://dockerhub.icu",

    "https://docker.registry.cyou",

    "https://docker-cf.registry.cyou",

    "https://dockercf.jsdelivr.fyi",

    "https://docker.jsdelivr.fyi",

    "https://dockertest.jsdelivr.fyi",

    "https://mirror.aliyuncs.com",

    "https://dockerproxy.com",

    "https://mirror.baidubce.com",

    "https://docker.m.daocloud.io",

    "https://docker.nju.edu.cn",

    "https://docker.mirrors.sjtug.sjtu.edu.cn",

    "https://docker.mirrors.ustc.edu.cn",

    "https://mirror.iscas.ac.cn",

    "https://docker.rainbond.cc"

    ]

}

第三步重启docker:

[vagrant@localhost etc]$ sudo systemctl daemon-reload

[vagrant@localhost etc]$ sudo systemctl restart docker

第四步拉取镜像:

sudo docker pull mysql:5.7

如拉取失败更改DNS配置

sudo vi /etc/resolv.conf

新增nameserver 8.8.8.8

Nameserver8.8.4.4

启动MySQL镜像:

sudo docker pull mysql:5.7

启动MySQL容器:

docker run -d --name mysql-new -e MYSQL_ROOT_PASSWORD=123456

设置账户可以被本机navicate连接:

第一步:登录进入MySQL容器内部

docker exec -it mysql-new mysql -u root -p
输入密码之后:

GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456' WITH GRANT OPTION;

完成容器配置

二、配置可以本机连接

docker exec -it mysql-new  bash

设置账户可以被本机navicate连接:

先进入MySQL内部

mysql -uroot -p123456

输入密码123456登录MySQL内部

GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456' WITH GRANT OPTION;

完成容器配置,这时候就可以通过navicat连接虚拟机的MySQL了

其他MySQL服务器重复以上步骤就可以

三、配置主从同步

一、进入容器

docker exec -it mysql-new  bash

二、修改文件配置

Vim etc/my.cof

新增以下内容:

log-bin=mysql-bin #开启日志

server-id=1      #设置唯一编号

binlog-ignore-db=mysql   #忽略数据库名称

binlog-ignore-db=information_schema  #忽略数据库名称

binlog-do-db=test  #要同步的数据库名称

binlog_format=STATEMENT

exc之后wq保存并退出

执行docker restart mysql-new  重启MySQL服务

三、进入MySQL内部

一、配置主MySQL

mysql -uroot -p123456

给从机创建账号:后续从机使用此账号进行数据同步

GRANT REPLICATION SLAVE ON *.* TO 'slave'@'%' IDENTIFIED BY '123456';

show  master status;

记录对应的file和Position

二、配置从MySQL

登录从MySQL内部

Vim etc\my.cof

新增以下内容:

log-bin=mysql-bin

server-id=2

binlog-ignore-db=mysql

binlog-ignore-db=information_schema

binlog-do-db=test

binlog_format=STATEMENT

进入MySQL内部:

mysql -uroot -p123456

执行此命令:

-- 停止从库复制进程

STOP SLAVE;

-- 重置从库配置

RESET SLAVE;

-- 重新配置主从连接

CHANGE MASTER TO MASTER_HOST='192.168.60.139',

MASTER_USER='slave',

MASTER_PASSWORD='123456',

MASTER_PORT=3306, 

MASTER_LOG_FILE='mysql-bin.000002',MASTER_LOG_POS=154;

-- 启动从库复制进程

START SLAVE;

-- 检查从库状态

SHOW SLAVE STATUS\G

配置主从同步完成

四、验证主从同步

主表

从表

在主表新增

888    老八    江西省
8888    Lucy    八佰陆i

此时从表也新增两条数据,主从同步完成。

五、停止主从同步

如何停止从服务复制功能

stop slave; 

如何重新配置主从 (即清除之前的主从配置)

stop slave;
reset master;

六、Java代码集成MySQL读写分离 

实体类:

package com.wwy.readwrite.domain.po;

import lombok.Data;

@Data
public class User {

    /**
     * 用户id
     */
    private String id;

    /**
     * 用户名
     */
    private String name;

    /**
     * 地址
     */
    private String address;
}
package com.wwy.readwrite.mapper;

import com.wwy.readwrite.domain.po.User;

public interface UserMapper{

    void insertUser(User user);

    User selectUserById(String id);

    User selectUserByName(String name);

}
<?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="com.wwy.readwrite.mapper.UserMapper">


    <insert id="insertUser">
        insert into user (id,name,address) value (#{id},#{name},#{address});
    </insert>
    <select id="selectUserById" resultType="com.wwy.readwrite.domain.po.User">
        select * from user where id = #{id}
    </select>
    <select id="selectUserByName" resultType="com.wwy.readwrite.domain.po.User">
           select * from user where name = #{name}
    </select>
</mapper>
logging:
  level:
    com.itheima: debug
  pattern:
    dateformat: HH:mm:ss
mybatis:
  mapper-locations: classpath*:mapper/*.xml
master:
  datasource:
    jdbc-url: jdbc:mysql://192.168.60.139:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULL
    username: root
    password: 123456  #这里写真实的主库密码
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 30
      minimum-idle: 10
      max-lifetime: 1800000
      connection-timeout: 30000
      idle-timeout: 600000
      connection-test-query: SELECT 1
      read-only: false
slave:
  datasource:
    jdbc-url: jdbc:mysql://192.168.60.140:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULL
    username: root
    password: 123456  #这里写真实的从库密码
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 30
      minimum-idle: 10
      max-lifetime: 1800000
      connection-timeout: 30000
      idle-timeout: 600000
      connection-test-query: SELECT 1
      read-only: true
读写分离相关配置:

在resource目录下新增mybatis-config.xml配置文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 配置全局属性 -->
    <settings>
        <!-- 使用jdbc的getGeneratedKeys获取数据库自增主键值 -->
        <setting name="useGeneratedKeys" value="true" />

        <!-- 使用列别名替换列名 默认:true -->
        <setting name="useColumnLabel" value="true" />

        <!-- 开启驼峰命名转换:Table{create_time} -> Entity{createTime} -->
        <setting name="mapUnderscoreToCamelCase" value="true" />
        <!-- 打印查询语句 -->
        <setting name="logImpl" value="STDOUT_LOGGING" />
    </settings>
    <plugins>
        <!--        使Mybatis拦截器生效,如果没有下面配置的话,Mybatis拦截器就不会生效-->
        <plugin interceptor="com.wwy.readwrite.config.DynamicDataSourceInterceptor"/>
    </plugins>
</configuration>

配置数据源:

package com.wwy.readwrite.config;

/**
 * @author 王伟羽
 * @program: readwrite-demo
 * @description:
 * @create: 2025/02/08
 */

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
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.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;

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

/**
 * dao层配置
 */
@Configuration
@MapperScan(basePackages = "com.example.masterslavedb.dao", sqlSessionFactoryRef = "sqlSessionFactoryBean")
public class SpringDaoConf {
    /**
     * 主库数据源
     * @return
     */
    @Bean(name = "masterDataSource")
    @ConfigurationProperties(prefix = "master.datasource")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

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

    /**
     * 我们自己实现的动态数据源
     * @return
     */
    @Bean(name = "dynamicDataSource")
    public DynamicDataSource dynamicDataSource() {
        DynamicDataSource result = new DynamicDataSource();
        Map<Object, Object> targetDataSources = new HashMap<Object, Object>();
        targetDataSources.put("master", masterDataSource());
        targetDataSources.put("slave", slaveDataSource());
        result.setTargetDataSources(targetDataSources);
        return result;
    }

    /**
     * 因为要在确定了sql语句之后才能确定选择哪个数据源,所以我们需要一个懒加载数据源
     * @return
     */
    @Bean(name = "dataSourceProxy")
    public LazyConnectionDataSourceProxy dataSourceProxy() {
        LazyConnectionDataSourceProxy result = new LazyConnectionDataSourceProxy();
        result.setTargetDataSource(dynamicDataSource());
        return result;
    }

    @Bean(name = "sqlSessionFactoryBean")
    public SqlSessionFactoryBean sqlSessionFactoryBean() throws IOException {
        SqlSessionFactoryBean result = new SqlSessionFactoryBean();
        result.setDataSource(dataSourceProxy());
        //设置entity包扫描路径
        result.setTypeAliasesPackage("com.wwy.readwrite.domain.po");
        //注入mybatis配置
        result.setConfigLocation(new PathMatchingResourcePatternResolver().getResource("classpath:mybatis-config.xml"));
        //设置sql映射文件包扫描路径
        result.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml"));
        return result;
    }

    @Bean(name = "transactionManager")
    public DataSourceTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSourceProxy());
    }

}

配置数据源路由器,使其找到对应的数据源:

package com.wwy.readwrite.config;

/**
 * @author 王伟羽
 * @program: readwrite-demo
 * @description:
 * @create: 2025/02/08
 */

import com.wwy.readwrite.utlis.DynamicDataSourceHolder;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;

/**
 * 数据源路由器,用于自动将sql语句分发给对应的数据源
 * 比如将增删改sql语句分发给主库数据源,将查询sql语句分发给从库数据源
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DynamicDataSourceHolder.getDbType();
    }
}

设置拦截器,拦截sql请求并分析其对应的方法

package com.wwy.readwrite.config;

/**
 * @author 王伟羽
 * @program: readwrite-demo
 * @description:
 * @create: 2025/02/08
 */

import com.wwy.readwrite.utlis.DynamicDataSourceHolder;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.util.Locale;
import java.util.regex.Pattern;

/**
 * mybatis拦截器
 */
@Intercepts(
        //mybatis会将增删改的操作封装在update里面,所以insert和delete就不需要写了
        注意这里的Executor所属的包是org.apache.ibatis.executor
        {@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
                @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
        })
public class DynamicDataSourceInterceptor implements Interceptor {
    private static final Logger LOGGER = LoggerFactory.getLogger(DynamicDataSourceInterceptor.class);
    private static final Pattern PATTERN = Pattern.compile(".*insert\\u0020.*|.*delete\\u0020.*|.*update\\u0020.*");

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //判断本次执行是否被事务管理
        boolean synchronizationActive = TransactionSynchronizationManager.isActualTransactionActive();
        //获取mybatis传过来的参数
        Object[] args = invocation.getArgs();
        //sql是属于哪种操作(增删改查)往往是保存在第一个对象中
        MappedStatement ms = (MappedStatement) args[0];
        String lookupKey = DynamicDataSourceHolder.DB_MASTER;
        if (!synchronizationActive) {
            //如果是读操作
            if (ms.getSqlCommandType().equals(SqlCommandType.SELECT)) {
                //selectKey为自增id查询主键 SELECT LAST_INSERT_ID()方法,使用主库
                if (ms.getId().contains(SelectKeyGenerator.SELECT_KEY_SUFFIX)) {
                    lookupKey = DynamicDataSourceHolder.DB_MASTER;
                } else {
                    BoundSql boundSql = ms.getSqlSource().getBoundSql(args[1]);
                    String sql = boundSql.getSql().toLowerCase(Locale.CHINA)
                            .replaceAll("[\\t\\n\\r]", " ");//将所有的制表符换行符回车符替换掉
                    //匹配sql语句是否为增删改
                    if (PATTERN.matcher(sql).matches()) {
                        lookupKey = DynamicDataSourceHolder.DB_MASTER;
                    } else {
                        lookupKey = DynamicDataSourceHolder.DB_SLAVE;
                    }
                }
            }
        } else {
            //用事务管理的操作一般都是写操作,因此我们用主库
            lookupKey = DynamicDataSourceHolder.DB_MASTER;
        }
        LOGGER.debug("设置方法[{}], use[{}], Strategy, SqlCommandType[{}]...",
                ms.getId(), lookupKey, ms.getSqlCommandType().name());
        DynamicDataSourceHolder.setDbType(lookupKey);
        return invocation.proceed();
    }
}

package com.wwy.readwrite.utlis;

/**
 * @author 王伟羽
 * @program: readwrite-demo
 * @description:
 * @create: 2025/02/08
 */

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 一个工具类,mybatis拦截器会把获取主库还是获取从库的通知放在这个类的contextHolder中,
 * 数据源路由器再从这个contextHolder中获取通知
 */
public class DynamicDataSourceHolder {
    private static final Logger LOGGER = LoggerFactory.getLogger(DynamicDataSourceHolder.class);
    private static ThreadLocal<String> contextHolder = new ThreadLocal<String>();
    public static final String DB_MASTER = "master";
    public static final String DB_SLAVE = "slave";

    /**
     * 获取线程的DbType
     * @return
     */
    public static String getDbType() {
        String result = contextHolder.get();
        if (result == null) {
            result = DB_MASTER;
        }
        return result;
    }

    /**
     * 设置线程的DbType
     * @param dbType
     */
    public static void setDbType(String dbType) {
        LOGGER.info("使用的数据源为:" + dbType);
        contextHolder.set(dbType);
    }

    /**
     * 清除数据源类型
     */
    public static void removeDbType() {
        contextHolder.remove();
    }
}

配置完成之后测试:

package com.wwy.readwrite.mapper;

import com.wwy.readwrite.domain.po.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    void testInsert() {
        User user = new User();
        user.setId("999");
        user.setName("测试插入主库");
        user.setAddress("测试");
        userMapper.insertUser(user);
    }
    


    @Test
    void testQueryByIds() {
        User user = userMapper.selectUserByName("测试插入主库");
        System.out.println("user = " + user);
    }


}

执行插入方法

可以看到此时是直接使用主库连接的,因为主从还设置了主从同步,所以从库也会有新增的数据

执行查询逻辑:

可以看到此时查询的数据源就变成了从库,至此已完成读写分离。

代码地址:

read_write: MySQL读写分离

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值