本篇主要有兩部分:
-
1、使用docker部署mysql主從 實現主從復制
-
2、springboot項目多數據源配置,實現讀寫分離
一、使用docker部署mysql主從 實現主從復制
此次使用的是windows版本docker,mysql版本是5.7
-
1、使用docker獲取mysql鏡像
docker pull mysql:5.7.23 #拉取鏡像文件
docker images #查看鏡像文件
-
2、使用docker運行mysql master
docker run --name mysql-master --privileged=true -v F:\dockerV\mysql:/var/lib/mysql -p 3307:3306 -e MYSQL_ROOT_PASSWORD=654321 -d mysql:5.7.23
-
-
--name 容器名稱mysql-master
-
--privileged 指定了當前容器是否真正的具有root權限,所謂的root權限是指具有宿主機的root權限,而不僅僅只是在容器內部有root權限
-
-v 將系統的F:\dockerV\mysql掛載到容器的/var/lib/mysql,注意是將宿主機 掛載到 容器內部,而不是將容器內部掛載到宿主機
-
-p 表示宿主機上的某個端口映射到docker容器內的某個端口,這里也就是將宿主機的3307端口映射到容器內部的3306端口
-
-e 表示指定當前容器運行的環境變量,該變量一般在容器內部程序的配置文件中使用,而在外部運行容器指定該參數。這里的MYSQL_ROOT_PASSWORD表示容器內部的MySQL的啟動密碼
-
-d 后台運行,鏡像文件為mysql:5.7.23
-
-
接下來進入容器內部,修改配置,使其作為mysql master運行
docker exec -it mysql-master bash #進入容器內部
- 配置mysql master,修改mysql.cnf
- 使用vim修改mysql.cnf,沒有安裝vim會提示bash: vi: command not found 則需要安裝vim
apt-get install vim
apt-get update
apt-get install vim
vim mysqld.cnf #修改cnf文件,添加 server-id 表示master服務標識,同一局域網內注意要唯一 和 log-bin=mysql-bin 開啟二進制日志功能,可以隨便取,用來完成主從復制
- 修改完成mysql的配置后,需要重啟服務生效
service MySQL restart # 重啟mysql服務時會使得docker容器停止,我們還需要docker start mysql-master啟動容器
docker start mysql-master #啟動容器
- 連接mysql客戶端,創建用來完成主從復制的賬號
mysql -uroot -p654321 #連接mysql
- 為從服務器創建一個可以用來操作master服務器的賬戶,也就是創建一個專門用來復制binlog的賬號,並且賦予該賬號復制權限,其命令如下
grant replication slave on *.* to 'slaveaccount'@'%' identified by '654321'; #賬號slaveaccount 密碼654321
flush privileges; #刷新用戶權限表
show master status #查看mysql master的狀態
記錄下上的position和file 在創建MySQL slave配置時會用到
到這里mysql master 已經配置完成了
-
3、使用docker運行mysql slave
docker run --name mysql-slave --privileged=true -v F:\dockerV\mysql-slave:/var/lib/mysql -p 3308:3306 --link mysql-master:master -e MYSQL_ROOT_PASSWORD=654321 -d mysql:5.7.23
mysql slave 的參數主要和master有兩點不同
-
所映射的宿主機的端口號不能與master容器相同,因為其已經被master容器占用;
-
必須加上--link參數,其后指定了當前容器所要連接的容器,mysql-master表示所要連接的容器的名稱,master表示為該容器起的一個別名,通俗來講,就是slave容器通過這兩個名稱都可以訪問到master容器。這么做的原因在於,如果master與slave不在同一個docker network中,那么這兩個容器相互之間是沒法訪問的。
docker exec -it mysql-slave /bin/bash #進入mysql salve容器
vim mysqld.cnf #修改cnf文件,添加 server-id 表示slave服務標識,如果此salve需要作為其他mysql的主,那么就需要配置log-bin=mysql-bin
service MySQL restart # 重啟mysql服務時會使得docker容器停止,我們還需要docker start mysql-slave啟動容器
docker start mysql-master #啟動容器
mysql -uroot -proot #連接mysql服務
配置mysql slave 使其以slave模式運行
change master to master_host='172.17.0.2', master_user='slaveaccount', master_password='654321', master_port=3306, master_log_file='mysql-bin.000001', master_log_pos=2272, master_connect_retry=30;
注意:這一步主要在slave是配置master的信息,包括 master的 地址、端口、賬號、密碼、log文件、log文件偏移量、重試。下面介紹這些參數值從何獲取
-
獲取master_host,使用docker inspect mysql-master查看master容器元數據。其中 IPAdress是master_host
-
獲取master_port,是運行master映射容器內部的端口,這里就是3306
-
獲取master_user和master_password,是第一步中在master中創建的slave賬號
-
獲取master_log_file和master_log_pos,是配置master中最后一步使用show master status獲取的文件和偏移量
-
獲取master_connect_retry,是slave重試連接master動作前的休眠時間,單位s,默認60s
start slave; #以slave模式運行
show salve status \G; #查看slave的狀態
可以看到,Slave_IO_Running:YES和Slave_SQL_Running:YES 證明此時主從復制已經就緒,slave配置到此完成
-
4、驗證主從復制效果
- 這里我們在連接master,並創建一張表,添加數據,在從庫查看數據是否同步。
- 在主庫創建數據,並在從庫查看數據是否同步成功。
mysql> create database test;
Query OK, 1 row affected (0.01 sec)
mysql> use test;
Database changed
mysql> create table t_user(id bigint, name varchar(255));
Query OK, 0 rows affected (0.02 sec)
mysql> insert into t_user(id, name) value (1, 'cgg');
Query OK, 1 row affected (0.01 sec)
mysql> select * from test.t_user;
+------+------+
| id | name |
+------+------+
| 1 | cgg |
+------+------+
1 row in set (0.00 sec)
-
5、mysql主從復制原理
- 主庫db的更新事件(update、insert、delete)被寫到binlog
- 主庫創建一個binlog dump thread,把binlog的內容發送到從庫
- 從庫啟動並發起連接,連接到主庫
- 從庫啟動之后,創建一個I/O線程,讀取主庫傳過來的binlog內容並寫入到relay log
- 從庫啟動之后,創建一個SQL線程,從relay log里面讀取內容,從Exec_Master_Log_Pos位置開始執行讀取到的更新事件,將更新內容寫入到slave的db
二、springboot項目多數據源配置,實現讀寫分離
-
1、主從多數據源配置
- yml配置
server:
port: 8888
servlet:
encoding:
charset: UTF-8
force: true
enabled: true
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://127.0.0.1:3307/test?serverTimezone=GMT%2B8&characterEncoding=utf-8&autoReconnect=true&failOverReadOnly=false&useSSL=false
username: root
password: 654321
slave:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://127.0.0.1:3308/test?serverTimezone=GMT%2B8&characterEncoding=utf-8&autoReconnect=true&failOverReadOnly=false&useSSL=false
username: root
password: 654321
- 數據源配置
/**
* @author cgg
**/
@Configuration
@EnableTransactionManagement
public class DynamicDataSourceConfig {
@Bean(name = "slaveDatasource")
@ConfigurationProperties(prefix = "slave.datasource")
public DataSource dbSlave() {
return DruidDataSourceBuilder.create().build();
}
@Bean(name = "dataSource")
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dbMaster() {
return DruidDataSourceBuilder.create().build();
}
@Bean
@Primary
public DataSource multipleDataSource(@Qualifier("dataSource") DataSource db,
@Qualifier("slaveDatasource") DataSource slaveDatasource) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
Map<Object, Object> targetDataSources = new HashMap<>(16);
targetDataSources.put("dataSource", db);
targetDataSources.put("slaveDatasource", slaveDatasource);
dynamicDataSource.setTargetDataSources(targetDataSources);
//設置默認數據源為從庫,如果寫操作業務多,可以默認設置為主庫
dynamicDataSource.setDefaultTargetDataSource(slaveDatasource);
return dynamicDataSource;
}
@Bean("sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory() throws Exception {
MybatisSqlSessionFactoryBean sqlSessionFactory = new MybatisSqlSessionFactoryBean();
sqlSessionFactory.setDataSource(multipleDataSource(dbSlave(), dbMaster()));
MybatisConfiguration configuration = new MybatisConfiguration();
configuration.setJdbcTypeForNull(JdbcType.NULL);
configuration.setMapUnderscoreToCamelCase(true);
configuration.setCacheEnabled(false);
sqlSessionFactory.setConfiguration(configuration);
sqlSessionFactory.setMapperLocations((new PathMatchingResourcePatternResolver()).getResources(DEFAULT_MAPPER_LOCATION));
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
paginationInterceptor.setOverflow(false);
paginationInterceptor.setLimit(-1);
paginationInterceptor.setCountSqlParser(tenantSqlParserCountOptimize());
Interceptor[] plugins = new Interceptor[]{new ShardTableInterceptor(), paginationInterceptor};
sqlSessionFactory.setPlugins(plugins);
return sqlSessionFactory.getObject();
}
}
/**
* 動態數據源
*
* @author cgg
**/
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DbContextHolder.getDbType();
}
}
-
2、配置切面控制主從數據源切換、讀從庫寫主庫,實現讀寫分離
- aop切面
/**
* @author cgg
**/
@Component
@Order(value = -100)
@Slf4j
@Aspect
public class DataSourceSwitchAspect {
//master 包下的操作都是操作主庫業務
@Pointcut("execution(* com.master..*.*(..))")
private void db1Aspect() {
}
//slave 包下的操作都是操作從庫業務
@Pointcut("execution(* com.slave.*.*(..))")
private void db2Aspect() {
}
@Before("db1Aspect()")
public void dbMaster() {
log.debug("切換到Master 數據源...");
DbContextHolder.setDbType("dataSource");
}
@Before("db2Aspect()")
public void dbSlave() {
log.debug("切換到Slave 數據源...");
DbContextHolder.setDbType("slaveDatasource");
}
}
/**
* @author cgg
**/
public class DbContextHolder {
private static final ThreadLocal contextHolder = new ThreadLocal<>();
/**
* 設置數據源
* @param dbType
*/
public static void setDbType(String dbType) {
contextHolder.set(dbType);
}
/**
* 取得當前數據源
* @return
*/
public static String getDbType() {
return (String) contextHolder.get();
}
/**
* 清除上下文數據
*/
public static void clearDbType() {
contextHolder.remove();
}
}
-
3、驗證讀寫分離效果
- 使用接口調用不同接口,切點會自動切換數據源,這里寫庫接口只會操作主庫,讀庫接口會操作從庫。實現讀寫分離