首頁 > 軟體

MySQL主從複製的原理圖解及Java語言範例使用

2022-08-12 14:05:20

概述

實際生產的過程中為了實現資料庫的高可用,不會只有一個資料庫節點。至少會搭建主從複製的資料庫架構,從庫可以作為主庫的資料備份,以免主資料庫損壞的情況下丟失資料;當存取量增加的時候可以作為讀節點承擔部分流量等。下面就進行從零開始搭建MySQL的主從架構。

主從複製原理

以MySQL一主兩從架構為為例,也就是一個master節點下有兩個slave節點,在這套架構下,寫操作統一交給master節點,讀請求交給slave節點處理。

為了保證master節點和slave節點資料一致,在master節點寫入資料後,會同時將資料複製到對應的slave節點。主從複製資料的過程中會用到三個執行緒,master節點上的binlog dump執行緒,slave節點的IO執行緒和SQL執行緒。

主從複製的核心流程:

  • 當master節點接收到一個寫請求時,這個寫請求可能是增刪改操作,此時會把寫請求的操作都記錄到binlog紀錄檔中。
  • master節點會把資料賦值給slave節點,如圖中的兩個slave節點。這個過程首先得要每個slave節點連線到master節點上,當slave節點連線到master節點上時,master節點會為每一個slave節點分別建立一個binlog dump執行緒,用於向每個slave節點傳送binlog紀錄檔。
  • 此時,binlog dump執行緒會讀取master節點上的binlog紀錄檔,然後將binlog紀錄檔傳送給slave節點上的I/O執行緒。
  • slave幾點上的I/O執行緒接收到binlog日之後,會將binlog紀錄檔先寫入到原生的relaylog中,relaylog中就儲存了master的binlog紀錄檔。
  • 最後,slave節點上的SQL執行緒會讀取relaylog中的biinlog紀錄檔,將其解析成具體的增刪改操作,把這些在master節點上進行過的操作,重新在slave節點上也重做一遍,打到資料還原的效果,這樣就可以保證master節點和slave節點的資料一致性了。

主從複製模式

MySQL的主從複製模式分為:全同步複製,非同步複製,半同步複製,增強半同步複製。

全同步複製

全同步複製,就是當主庫執行完一個事物之後,要求所有的從庫也都必須執行完該事務,才可以返回處理結果給使用者端;因此雖然全同步複製資料一致性得到保證了,但是主庫完成一個事物需要等待所有從庫也完成,效能就比較低了。

非同步複製

非同步複製,當主庫提交事務後會通知binlog dump執行緒傳送binlog紀錄檔給從庫,一旦binlog dump執行緒將binlog紀錄檔傳送給從庫之後,不需要等到從庫也同步完成事務,主庫就會講處理結果返回給使用者端。

因為主庫只管自己執行完事務,就可以將處理結果返回給使用者端,而不用關係從庫是否執行完事務,這就可能導致短暫的主從資料不一致的問題了,比如剛在主庫插入的資料,如果馬上在從庫查詢就可能查詢不到。

當主庫提交食物後,如果宕機掛掉了,此時可能binlog還沒來得及同步給從庫,這時候如果為了回覆故障切換主從節點的話,就會出現資料丟失的問題,所以非同步複製雖然效能高,但資料一致性上是比較弱的。

MySQL預設採用的是非同步複製模式。

半同步複製

半同步複製就是在同步複製和非同步中做了折中選擇,我們可以結合著MySQL官網來看下是半同步和主從複製的過程。

當主庫提交事務後,至少還需要一個從庫返回接收到binlog紀錄檔,併成功寫入到relaylog的訊息,這個的時候,主庫才會講處理結果返回給使用者端。

相比前兩種複製方式,半同步複製較好地兼顧了資料一致性以及效能損耗的問題。

同時,半同步複製也存在以下幾個問題:

  • 半同步複製的效能,相比非同步複製而言有所下降,因為需要等到等待至少一個從庫確認接收到binlog紀錄檔的響應,所以新能上是有所損耗的。
  • 主庫等待從庫響應的最大時長我們是可以設定的,如果超過了我們設定的事件,半同步複製就會變成非同步複製,那麼,非同步複製的問題同樣也就出現了。
  • 在MySQL5.7.2之前的版本中,半同步複製存在幻讀問題。當主庫成功提交事務並處於等待從庫確認的過程中,這個時候,從庫都還沒來得及返回處理結果給使用者端,但因為主庫儲存引擎內部已經提交事務了,所以,其他使用者端是可以到主庫中讀到資料的。但是,如果下一秒主庫宕機,下次請求過來只能讀取從庫,因為從庫還沒有從主庫同步資料,所以從庫中讀取不到這條資料了,和上一次讀取資料的結果相比,就造成了幻讀的現象。

增強半同步複製

增強半同步複製是MySQL5.7.2後的版本對半同步複製做的一個改進,原理幾乎是一樣的,主要是解決幻讀的問題。

主庫設定了引數rpl_semi_sync_master_wait_point=AFTER_SYNC後,主庫在儲存引擎提交事務前,必須先首都哦啊從庫資料同步完成的確認資訊後,才能提交事務,以此來解決幻讀問題。

主從同步實戰

準備資料來源

config/datasource.properties

# masters
spring.datasource.masters.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.masters.url=jdbc:mysql://192.168.1.111:3306/monomer_order?useUnicode=true&characterEncoding=utf8&useSSL=false&autoReconnect=true&zeroDateTimeBehavior=convertToNull
spring.datasource.masters.username=root
spring.datasource.masters.password=123456
# slaves
spring.datasource.slaves[0].driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.slaves[0].url=jdbc:mysql://192.168.1.112:3306/monomer_order?useUnicode=true&characterEncoding=utf8&useSSL=false&autoReconnect=true&zeroDateTimeBehavior=convertToNull
spring.datasource.slaves[0].username=root
spring.datasource.slaves[0].password=123456

設定資料來源

package com.xinxin.order.context.config;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import lombok.Data;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.*;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.util.CollectionUtils;
import javax.sql.DataSource;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Data
@Configuration
@PropertySource("classpath:config/datasource.properties")
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceConfig {
    /**
     * 主庫資料來源資訊
     */
    private Map<String, String> masters;
    /**
     * 從庫資料來源資訊
     */
    private List<Map<String, String>> slaves;
    @SneakyThrows
    @Bean
    public DataSource masterDataSource() {
        log.info("masters:{}", masters);
        if (CollectionUtils.isEmpty(masters)) {
            throw new Exception("主庫資料來源不能為空");
        }
        return DruidDataSourceFactory.createDataSource(masters);
    }
    @SneakyThrows
    @Bean
    public List<DataSource> slaveDataSources() {
        if (CollectionUtils.isEmpty(slaves)) {
            throw new Exception("從庫資料來源不能為空");
        }
        final ArrayList<DataSource> dataSources = new ArrayList<>();
        for (Map<String, String> slaveProperties : slaves) {
            log.info("slave:{}", slaveProperties);
            dataSources.add(DruidDataSourceFactory.createDataSource(slaveProperties));
        }
        return dataSources;
    }
    @Bean
    @Primary
    @DependsOn({"masterDataSource", "slaveDataSources"})
    public DataSource routingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
                                        @Qualifier("slaveDataSources") List<DataSource> slaveDataSources) {
        final Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DataSourceContextHolder.MASTER, masterDataSource);
        for (int i = 0; i < slaveDataSources.size(); i++) {
            targetDataSources.put(DataSourceContextHolder.SLAVE + i, slaveDataSources.get(i));
        }
        final DataSourceRouter dataSourceRouter = new DataSourceRouter();
        dataSourceRouter.setTargetDataSources(targetDataSources);
        dataSourceRouter.setDefaultTargetDataSource(masterDataSource);
        return dataSourceRouter;
    }
    @Bean
    public DataSourceTransactionManager dataSourceTransactionManager(
            @Qualifier("routingDataSource") DataSource routingDataSource) {
        return new DataSourceTransactionManager(routingDataSource);
    }
}

資料來源上下文切換

package com.xinxin.order.context.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@Slf4j
public class DataSourceContextHolder {
    public static final String MASTER = "master";
    public static final String SLAVE = "slave";
    private static ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
    public static void setDatasourceType(String dataSourceType) {
        if (StringUtils.isBlank(dataSourceType)) {
            log.error("dataSourceType為空");
        }
        log.info("設定dataSource: {}", dataSourceType);
        CONTEXT_HOLDER.set(dataSourceType);
    }
    public static String getDataSourceType() {
        return CONTEXT_HOLDER.get() == null ? MASTER : CONTEXT_HOLDER.get();
    }
    public static void remove() {
        CONTEXT_HOLDER.remove();
    }
}

資料來源路由實現類

package com.xinxin.order.context.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
@Slf4j
public class DataSourceRouter extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        log.info("當前資料來源為: {}", DataSourceContextHolder.getDataSourceType());
        return DataSourceContextHolder.getDataSourceType();
    }
}

資料來源切換註解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
    String value() default DataSourceContextHolder.MASTER;
}

動態資料來源切換切面

package com.xinxin.order.aspect;
import com.xinxin.order.annotation.ReadOnly;
import com.xinxin.order.context.config.DataSourceContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class DynamicDataSourceAspect implements Ordered {
    @Before(value = "execution(* *(..))&& @annotation(readOnly)")
    public void before(JoinPoint joinPoint, ReadOnly readOnly) {
        log.info(joinPoint.getSignature().getName() + "走從庫");
        DataSourceContextHolder.setDatasourceType(DataSourceContextHolder.SLAVE);
    }
    @After(value = "execution(* *(..))&& @annotation(readOnly)")
    public void after(JoinPoint joinPoint, ReadOnly readOnly) {
        log.info(joinPoint.getSignature().getName() + "清除資料來源");
        DataSourceContextHolder.remove();
    }
    @Override
    public int getOrder() {
        return 0;
    }
}

總結

專案整合讀寫分離主要是通過收到注入資料來源,並通過攔截器設定當前執行緒的資料來源型別,需要使用資料來源的地方會通過資料來源路由器讀取當前執行緒的資料來源型別後返回實際的資料來源進行資料庫的操作。

到此這篇關於Java MySQL主從複製的原理圖解及範例使用的文章就介紹到這了,更多相關Java MySQL主從複製內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com