1.简介
MySQL Replication (MySQL 主从复制) 是指数据可以从一个MySQL数据库服务器主节点复制到一个或多个从节点。MySQL 默认采用异步复制方式(一级主从大概50~100 us),这样从节点不用一直访问主服务器来更新自己的数据,数据的更新可以在远程连接上进行,从节点可以复制主数据库中的所有数据库或者特定的数据库,或者特定的表。
2.为什么要做主从复制
- 读写分离。在业务复杂的系统中,如果在生产环境中,有一句sql语句需要锁表,导致暂时不能使用读的服务,那么就很影响运行中的业务,使用主从复制,让主库负责写,从库负责读,这样,即使主库出现了锁表的情景,通过读从库也可以保证业务的正常运作。
- 做数据的热备
- 架构的扩展。业务量越来越大,I/O访问频率过高,单机无法满足,此时做多库的存储,降低磁盘I/O访问的频率,提高单个机器的I/O性能。
3.原理
MySQL主从复制涉及到三个线程,一个运行在主节点(log dump thread),其余两个(I/O thread, SQL thread)运行在从节点
-
主节点 binary log dump 线程
当从节点连接主节点时,主节点会创建一个log dump 线程,用于发送bin-log的内容。在读取bin-log中的操作时,此线程会对主节点上的bin-log加锁,当读取完成,甚至在发动给从节点之前,锁会被释放。
-
从节点 I/O 线程
当从节点上执行`start slave`命令之后,从节点会创建一个I/O线程用来连接主节点,请求主库中更新的bin-log。I/O线程接收到主节点binlog dump 进程发来的更新之后,保存在本地relay-log中。
-
从节点 SQL 线程
SQL线程负责读取relay log中的内容,解析成具体的操作并执行,最终保证主从数据的一致性。
对于每一个主从连接,都需要三个进程来完成。当主节点有多个从节点时,主节点会为每一个当前连接的从节点建一个binary log dump 进程,而每个从节点都有自己的I/O进程,SQL进程。从节点用两个线程将从主库拉取更新和执行分成独立的任务,这样在执行同步数据任务的时候,不会降低读操作的性能。比如,如果从节点没有运行,此时I/O进程可以很快从主节点获取更新,尽管SQL进程还没有执行。如果在SQL进程执行之前从节点服务停止,至少I/O进程已经从主节点拉取到了最新的变更并且保存在本地relay日志中,当服务再次起来之后,就可以完成数据的同步。
要实施复制,首先必须打开Master 端的binary log(bin-log)功能,否则无法实现。
因为整个复制过程实际上就是Slave 从Master 端获取该日志然后再在自己身上完全顺序的执行日志中所记录的各种操作。如下图所示:
复制的基本过程如下:
从节点上的I/O 进程连接主节点,并请求从指定日志文件的指定位置(或者从最开始的日志)之后的日志内容;主节点接收到来自从节点的I/O请求后,通过负责复制的I/O进程根据请求信息读取指定日志指定位置之后的日志信息,返回给从节点。返回信息中除了日志所包含的信息之外,还包括本次返回的信息的bin-log file 的以及bin-log position;从节点的I/O进程接收到内容后,将接收到的日志内容更新到本机的relay log中,并将读取到的binary log文件名和位置保存到master-info 文件中,以便在下一次读取的时候能够清楚的告诉Master“我需要从某个bin-log 的哪个位置开始往后的日志内容,请发给我”;Slave 的 SQL线程检测到relay-log 中新增加了内容后,会将relay-log的内容解析成在祝节点上实际执行过的操作,并在本数据库中执行。
4.实战
4.1 环境配置:
-
两台服务器,并在服务器上安装MySQL
-
或者装两台Linux系统的虚拟机,Ubuntu或者centos可随意。
-
再或者一台机器跑两个MySQL的实例,跑在两个不同的端口(如3306和3307)上。
-
MySQL版本号最好一致
-
建议关闭防火墙
-
注意以下配置的代码,主从要对应
4.2 修改配置
下面为两台ubuntu服务器,并且均安装MySQL5.7
-
配置主服务器
vim /etc/mysql/my.cnf
修改配置文件
[mysqld]
server-id=1
#配置唯一的server-id,不设置MySQL5.7以上会报错
log_bin=master-bin
#mysql会根据这个配置自动设置log_bin为on状态,即开启binlog
log_bin_index=master-bin.index
#置log_bin_index文件为你指定的文件名后跟.index获得master二进制文件名及位置
#默认情况下备份是主库的全部操作都会备份到从库,实际可能需要忽略某些库,可以在主库中增加如下配置:
# 不同步哪些数据库
binlog-ignore-db=mysql
# 只同步哪些数据库,除此之外,其他不同步
binlog-do-db=mysql
进入数据库,创建用于数据同步的账户(目的,让从服务器来复制数据)
mysql> create user repl;
mysql> GRANT REPLICATION SLAVE ON *.* TO 'repl'@'从xxx.xxx.xxx.xx' IDENTIFIED BY 'mysql';
mysql> flush privileges;
这个配置的含义就是创建了一个数据库用户repl,密码是mysql, 在从服务器使用repl这个账号和主服务器连接的时候,就赋予其REPLICATION SLAVE的权限, *.* 表面这个权限是针对主库的所有表的,其中xxx就是从服务器的ip地址。
重启数据库
service mysql restart
启动成功之后,查看我们的配置是否起作用
查看主库状态
此时,主数据库设置完毕
-
配置从服务器
vim /etc/mysql/my.cnf
修改配置文件
[mysqld]
server-id=2
#配置唯一的server-id,不设置MySQL5.7以上会报错,不能与主库相同
relay-log=slave-relay-bin
#定义relay_log的位置和名称
relay-log-index=slave-relay-bin.index
#同bin-log-index
重启数据库
service mysql restart
进入数据库,执行以下代码
mysql> change master to master_host='主xxx.xxx.xxx.xx',master_port=3306,master_user='repl',master_password='mysql',master_log_file='master-bin.000005',master_log_pos=0;
这里面的xxx是主服务器ip,同时配置端口,repl代表访问主数据库的用户上述步骤执行完毕后执行start slave启动配置:
mysql> start slave;
#关闭同步为stop slave
查看状态命令,\G表示换行查看
mysql> show slave status \G;
当圆圈中的状态显示为yes时,代表配置成功
5.从代码层面实现读写分离
代码环境是 Springboot+Mybatis+阿里druib 连接池。想要读写分离就需要配置多个数据源,在进行写操作是选择写的数据源(主库),读操作时选择读的数据源(从库)。
配置文件如下
spring:
datasource:
#主配置源
master:
name: test
jdbc-url: jdbc:mysql://192.168.0.1:3306/test?allowMultiQueries=true&useSSL=false&useUnicode=true&characterEncoding=utf-8
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
#从配置源
slave:
name: test
jdbc-url: jdbc:mysql://192.168.0.2:3306/test?allowMultiQueries=true&useSSL=false&useUnicode=true&characterEncoding=utf-8
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
Enum类,定义主库从库
package com.softlab.common.model;
/**
* @author : Ar1es
* @date : 2019/11/28
* @since : Java 8
*/
public enum DBTypeEnum {
MASTER, SLAVE;
}
ThreadLocal定义数据源切换
package com.softlab.provider.config;
import com.softlab.common.model.DBTypeEnum;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author : Ar1es
* @date : 2019/11/28
* @since : Java 8
*/
public class DBContextHolder {
/**
* ThreadLocal 不是 Thread,是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,对数据存储后,只有在线程中才可以获取到存储的数据,对于其他线程来说是无法获取到数据。
* 大致意思就是ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。
*/
private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();
public static void set(DBTypeEnum dbTypeEnum){
contextHolder.set(dbTypeEnum);
}
public static DBTypeEnum get() {
return contextHolder.get();
}
public static void master() {
set(DBTypeEnum.MASTER);
System.out.println("--------以下操作为master(写操作)--------");
}
public static void slave() {
set(DBTypeEnum.SLAVE);
System.out.println("--------以下操作为slave(读操作)--------");
}
public static void clear() {
contextHolder.remove();
}
}
重写路由选择类
package com.softlab.provider.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;
/**
* AbstractRoutingDataSource的getConnection() 方法根据查找 lookup key 键对不同目标数据源的调 用,通常是通过(但不一定)某些线程绑定的事物上下文来实现。
* AbstractRoutingDataSource的多数据源动态切换的核心逻辑是:在程序运行时,把数据源数据源通过 AbstractRoutingDataSource 动态织入到程序中,灵活的进行数据源切换。
* 基于AbstractRoutingDataSource的多数据源动态切换,可以实现读写分离,这么做缺点也很明显,无法动态的增加数据源。
* @author : Ar1es
* @date : 2019/11/28
* @since : Java 8
*/
public class MyRoutingDataSource extends AbstractRoutingDataSource {
/**
* determineCurrentLookupKey() 方法决定使用哪个数据源、
* 根据Key获取数据源的信息,上层抽象函数的钩子
*/
@Nullable
@Override
protected Object determineCurrentLookupKey() {
return DBContextHolder.get();
}
}
配置Mybatis SqlSessionFactory 和事务管理器
package com.softlab.provider.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.annotation.Resource;
import javax.sql.DataSource;
/**
* @author : Ar1es
* @date : 2019/11/28
* @since : Java 8
*/
@Configuration
@EnableTransactionManagement
public class MyBatisConfig {
@Value("${mybatis.mapper-locations}")
private String mapperLocation;
/**
* 注入自己重写的数据源
*/
@Resource(name = "myRoutingDataSource")
private DataSource myRoutingDataSource;
/**
* 配置SqlSessionFactory
* @return SqlSessionFactory
* @throws Exception
*/
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
//ResourcePatternResolver(资源查找器)定义了getResources来查找资源
//PathMatchingResourcePatternResolver提供了以classpath开头的通配符方式查询,否则会调用ResourceLoader的getResource方法来查找
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
sqlSessionFactoryBean.setMapperLocations(resolver.getResources(mapperLocation));
return sqlSessionFactoryBean.getObject();
}
/**
* 事务管理器
* 不写则事务不生效
*/
@Bean
public PlatformTransactionManager platformTransactionManager() {
return new DataSourceTransactionManager(myRoutingDataSource);
}
}
配置数据源
package com.softlab.provider.config;
import com.softlab.common.model.DBTypeEnum;
import org.springframework.beans.factory.annotation.Qualifier;
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 javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
/**
* 配置多数据源
* @author : Ar1es
* @date : 2019/11/28
* @since : Java 8
*/
@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();
}
/**
* 配置选择数据源
* @param masterDataSource
* @param slaveDataSource
* @return DataSource
*/
@Bean
public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slaveDataSource") DataSource slaveDataSource) {
Map<Object, Object> targetDataSource = new HashMap<>();
targetDataSource.put(DBTypeEnum.MASTER, masterDataSource);
targetDataSource.put(DBTypeEnum.SLAVE, slaveDataSource);
MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource();
//找不到用默认数据源
myRoutingDataSource.setDefaultTargetDataSource(masterDataSource);
//可选择目标数据源
myRoutingDataSource.setTargetDataSources(targetDataSource);
return myRoutingDataSource;
}
}
切面实现数据源切换
package com.softlab.provider.config;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 多数据源, 切面处理类
* @author : Ar1es
* @date : 2019/11/28
* @since : Java 8
*/
@Aspect
@Component
//需要service中方法名称按一定规则编写,然后通过切面来设置数据库类别
public class DataSourceAop {
//@annotation在方法上进行设置
@Pointcut("!@annotation(com.softlab.common.util.Master)" +
"&& (execution(* com.softlab.common.service..*.select*(..))" +
"|| execution(* com.softlab.common.service..*.get*(..)))")
public void readPointcut() {
}
@Pointcut("@annotation(com.softlab.common.util.Master)" +
"|| execution(* com.softlab.common.service..*.insert*(..))" +
"|| execution(* com.softlab.common.service..*.add*(..))" +
"|| execution(* com.softlab.common.service..*.update*(..))" +
"|| execution(* com.softlab.common.service..*.delete*(..)))")
public void writePointcut() {
}
@Before("readPointcut()")
public void read() {
DBContextHolder.slave();
}
@Before("writePointcut()")
public void write() {
DBContextHolder.master();
}
}
如果有强制走主库的操作,可以定义注解
package com.softlab.common.util;
/**
* @author : Ar1es
* @date : 2019/11/28
* @since : Java 8
*/
public @interface Master {
}
自行定义读写操作,结果如下图所示