springboot多資料來源配合docker部署mysql主從實現讀寫分離

coffeebabe 發表於 2021-09-23
MySQL Docker Spring

本篇主要有兩部分:

  • 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

修改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 開啟二進位制日誌功能,可以隨便取,用來完成主從複製

修改cnf檔案

  • 修改完成mysql的配置後,需要重啟服務生效

service MySQL restart # 重啟mysql服務時會使得docker容器停止,我們還需要docker start mysql-master啟動容器

docker start mysql-master #啟動容器

  • 連線mysql客戶端,建立用來完成主從複製的賬號

mysql -uroot -p654321 #連線mysql

連線mysql

  • 為從伺服器建立一個可以用來操作master伺服器的賬戶,也就是建立一個專門用來複制binlog的賬號,並且賦予該賬號複製許可權,其命令如下

grant replication slave on *.* to 'slaveaccount'@'%' identified by '654321'; #賬號slaveaccount 密碼654321

flush privileges; #重新整理使用者許可權表

show master status #檢視mysql master的狀態
檢視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_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的狀態

可以看到,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主從複製原理

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、驗證讀寫分離效果

    • 使用介面呼叫不同介面,切點會自動切換資料來源,這裡寫庫介面只會操作主庫,讀庫介面會操作從庫。實現讀寫分離