# Mysql 主从集群搭建文档,实现读写分离
最近在自己写的项目中需要应对大量的用户查询读写操作,一台服务器当然是不够的,所以在边学边敲的背景下,记录这篇笔记,从 0 开始搭建主从集群。
# 下面👇开始操作:
1. 分别在两台服务器搭建 mysql 服务
两台服务器的 IP 地址分别为主服务器(192.168.20.1)和从服务器(192.168.20.2)。
2. 配置文件 my.cnf 的修改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 根据上一篇文章,编辑my.cnf文件 [root@localhost mysql]# vim /etc/my.cnf # 在[mysqld]中添加: server-id=1 log_bin=master-bin log_bin_index=master-bin.index binlog_do_db=test # 备注: # server-id 服务器唯一标识。 # log_bin 启动MySQL二进制日志,即数据同步语句,从数据库会一条一条的执行这些语句。 # binlog_do_db 指定记录二进制日志的数据库,即需要复制的数据库名,如果复制多个数据库,重复设置这个选项即可。 # binlog_ignore_db 指定不记录二进制日志的数据库,即不需要复制的数据库名,如果有多个数据库,重复设置这个选项即可。 # 其中需要注意的是,binlog_do_db和binlog_ignore_db为互斥选项,一般只需要一个即可。
3. 创建从服务器的用户和权限
1 2 3 4 5 6 7 8 9 10 11 12 13 14 # 进入mysql数据库 [root@localhost mysql]# mysql -uroot -p Enter password: # 创建从数据库的root用户和权限 mysql> grant replication slave on *.* to root@'192.168.20.%' identified by 'Lgq081538' ; grant replication slave on *.* to '123456' # 备注 # 192.168.20.%通配符,表示0-255的IP都可访问主服务器,正式环境请配置指定从服务器IP # 若将 192.168.20.% 改为 %,则任何ip均可作为其从数据库来访问主服务器 # 退出mysql mysql> exit ;
4. 重启 mysql 服务
1 2 3 [root@localhost mysql]# service mysql restart Shutting down MySQL.... SUCCESS! Starting MySQL. SUCCESS!
5. 查看主服务器状态
1 2 3 4 5 6 7 8 9 10 11 12 # 进入mysql数据库 [root@localhost mysql]# mysql -uroot -p Enter password: # 查看主服务器状态 mysql> show master status; +-------------------+----------+--------------+------------------+-------------------+ | File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set | +-------------------+----------+--------------+------------------+-------------------+ | master-bin.000001 | 154 | test | | | +-------------------+----------+--------------+------------------+-------------------+ 1 row in set (0.00 sec)
6.slave 从服务器的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 配置文件my.cnf的修改 # 根据上一篇文章,编辑my.cnf文件 [root@localhost mysql]# vim /etc/my.cnf # 在[mysqld]中添加: server-id=2 relay-log=slave-relay-bin relay-log-index=slave-relay-bin.index # replicate-do-db=test # 备注: # server-id 服务器唯一标识,如果有多个从服务器,每个服务器的server-id不能重复,跟IP一样是唯一标识,如果你没设置server-id或者设置为0,则从服务器不会连接到主服务器。 # relay-log 启动MySQL二进制日志,可以用来做数据备份和崩溃恢复,或主服务器挂掉了,将此从服务器作为其他从服务器的主服务器。 # replicate-do-db 指定同步的数据库,如果复制多个数据库,重复设置这个选项即可。若在master端不指定binlog-do-db,则在slave端可用replication-do-db来过滤。 # replicate-ignore-db 不需要同步的数据库,如果有多个数据库,重复设置这个选项即可。 # 其中需要注意的是,replicate-do-db和replicate-ignore-db为互斥选项,一般只需要一个即可。
7. 重启 mysql 服务
1 2 3 [root@localhost mysql]# service mysql restart Shutting down MySQL.... SUCCESS! Starting MySQL. SUCCESS!
8. 连接 master 主服务器
1 2 3 4 5 6 7 8 9 10 11 12 # 进入mysql数据库 [root@localhost mysql]# mysql -uroot -p Enter password: # 连接master主服务器 mysql> change master to master_host='192.168.20.1' ,master_port=3306,master_user='root' ,master_password='123456' ,master_log_file='master-bin.000009' ,master_log_pos=473127; # 备注: # master_host对应主服务器的IP地址。 # master_port对应主服务器的端口。 # master_log_file对应show master status显示的File列:master-bin.000001。 # master_log_pos对应show master status显示的Position列:154。
9. 启动 slave 数据同步
1 2 3 4 5 6 7 # 启动slave数据同步 mysql> start slave; # 停止slave数据同步(若有需要) mysql> stop slave; 3.5 查看slave信息 mysql> show slave status\G; Slave_IO_Running和Slave_SQL_Running都为yes,则表示同步成功。
10. 测试
1 2 3 4 5 6 7 8 9 10 (1)在主服务器上登陆mysql,且进入test数据库,创建test表,且插入一条数据 提示:这里最好用数据库管理工具(如nacicat)来操作。 # 创建tb_test表 create table tb_test(ID varchar(36) primary key comment '主键ID',MEMO varchar(500) not null comment '信息'); # 插入一条数据 insert into tb_test(ID,MEMO) values('1','one test'); # 提交 commit; (2)在从服务器上登陆mysql,且进入test数据库 你会发现从数据库中,也出现了tb_test表,且表中还有one test数据存在,证明同步数据成功。
# 至此 Mysql 主从读写分离搭建完成
下面开始搭建 Spring Boot 项目中的相关配置以及实现👇
读写分离要做的事情就是对于一条 SQL 该选择哪个数据库去执行,至于谁来做选择数据库这件事,主要有两种实现方式,分别为:
1. 使用中间件,比如 Atlas,cobar,TDDL,mycat,heisenberg,Oceanus,vitess,OneProxy 等
2. 使用程序自己实现,利用 Spring Boot 提供的路由数据源以及 AOP,实现起来简单快捷
我们使用第二种方式 Spring Boot 数据源路由 + AOP ,这样能更好的控制流程,也便于后期提升性能;
代码实现
1. 首先配置下 pom.xml 因为我们使用 aop 实现,所以需要 aop 依赖
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency >
2. 数据源路由类功能 RoutingDataSource.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;public class RoutingDataSource extends AbstractRoutingDataSource { @Override protected Object determineCurrentLookupKey () { return DBContext.get(); } }
3. 数据源上下文类 DBContext.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 import lombok.extern.slf4j.Slf4j;import java.util.concurrent.atomic.AtomicInteger;@Slf4j public class DBContext { private static final ThreadLocal<DBTypeEnum> dbContext = new ThreadLocal <>(); private static final AtomicInteger counter = new AtomicInteger (-1 ); public static void set (DBTypeEnum dbType) { dbContext.set(dbType); } public static DBTypeEnum get () { return dbContext.get(); } public static void master () { set(DBTypeEnum.MASTER); log.info("切换到master库" ); } public static void slave () { int index = counter.getAndIncrement() % 2 ; log.info("slave库访问线程数==>{}" , counter.get()); if (index == 0 ) { set(DBTypeEnum.SLAVE1); log.info("切换到slave1库" ); } else { set(DBTypeEnum.SLAVE2); log.info("切换到slave2库" ); } } }
4. 数据库枚举类 DBTypeEnum.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.example.demo.databases;public enum DBTypeEnum { MASTER, SLAVE1, SLAVE2 }
这里我们配置三个库,分别是一个写库 Master,2 个读库 slave1,slave2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 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;@Configuration public class DataSourceConfigs { @Bean @ConfigurationProperties("spring.datasource.master") public DataSource masterDataSource () { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.slave1") public DataSource slave1DataSource () { return DataSourceBuilder.create().build(); } @Bean @ConfigurationProperties("spring.datasource.slave2") public DataSource slave2DataSource () { return DataSourceBuilder.create().build(); } @Bean public DataSource myRoutingDataSource (@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("slave1DataSource") DataSource slave1DataSource, @Qualifier("slave2DataSource") DataSource slave2DataSource) { Map<Object, Object> targetDataSources = new HashMap <>(); targetDataSources.put(DBTypeEnum.MASTER, masterDataSource); targetDataSources.put(DBTypeEnum.SLAVE1, slave1DataSource); targetDataSources.put(DBTypeEnum.SLAVE2, slave2DataSource); RoutingDataSource routingDataSource = new RoutingDataSource (); routingDataSource.setDefaultTargetDataSource(masterDataSource); routingDataSource.setTargetDataSources(targetDataSources); return routingDataSource; } }
6. 切面类 DataSourceAop.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.springframework.stereotype.Component;@Aspect @Component public class DataSourceAop { @Pointcut("@annotation(com.example.demo.databases.Master) " + "|| execution(* com.example.demo.*.service..*.insert*(..)) " + "|| execution(* com.example.demo.*.service..*.create*(..)) " + "|| execution(* com.example.demo.*.service..*.save*(..)) " + "|| execution(* com.example.demo.*.service..*.add*(..)) " + "|| execution(* com.example.demo.*.service..*.update*(..)) " + "|| execution(* com.example.demo.*.service..*.edit*(..)) " + "|| execution(* com.example.demo.*.service..*.delete*(..)) " + "|| execution(* com.example.demo.*.service..*.remove*(..))") public void writePointcut () { } @Pointcut("!@annotation(com.example.demo.databases.Master) " + "&& (execution(* com.example.demo.*.service..*.select*(..)) " + "|| execution(* com.example.demo.*.service..*.list*(..))" + "|| execution(* com.example.demo.*.service..*.count*(..))" + "|| execution(* com.example.demo.*.service..*.get*(..)))" ) public void readPointcut () { } @Before("writePointcut()") public void write () { DBContext.master(); } @Before("readPointcut()") public void read () { DBContext.slave(); } }
7. 注解类 Master.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.example.demo.databases;public @interface Master {}
# 关于我
Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!
非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!