MySQL主從復制原理和使用

概述

實際生產(chǎn)的過程中為了實現(xiàn)數(shù)據(jù)庫的高可用,不會只有一個數(shù)據(jù)庫節(jié)點。至少會搭建主從復制的數(shù)據(jù)庫架構(gòu),從庫可以作為主庫的數(shù)據(jù)備份,以免主數(shù)據(jù)庫損壞的情況下丟失數(shù)據(jù);當訪問量增加的時候可以作為讀節(jié)點承擔部分流量等。下面就進行從零開始搭建MySQL的主從架構(gòu)。

主從復制原理

以MySQL一主兩從架構(gòu)為為例,也就是一個master節(jié)點下有兩個slave節(jié)點,在這套架構(gòu)下,寫操作統(tǒng)一交給master節(jié)點,讀請求交給slave節(jié)點處理。

為了保證master節(jié)點和slave節(jié)點數(shù)據(jù)一致,在master節(jié)點寫入數(shù)據(jù)后,會同時將數(shù)據(jù)復制到對應的slave節(jié)點。主從復制數(shù)據(jù)的過程中會用到三個線程,master節(jié)點上的binlog dump線程,slave節(jié)點的I\O線程和SQL線程。



主從復制的核心流程:

當master節(jié)點接收到一個寫請求時,這個寫請求可能是增刪改操作,此時會把寫請求的操作都記錄到binlog日志中。

master節(jié)點會把數(shù)據(jù)賦值給slave節(jié)點,如圖中的兩個slave節(jié)點。這個過程首先得要每個slave節(jié)點連接到master節(jié)點上,當slave節(jié)點連接到master節(jié)點上時,master節(jié)點會為每一個slave節(jié)點分別創(chuàng)建一個binlog dump線程,用于向每個slave節(jié)點發(fā)送binlog日志。

此時,binlog dump線程會讀取master節(jié)點上的binlog日志,然后將binlog日志發(fā)送給slave節(jié)點上的I/O線程。

slave幾點上的I/O線程接收到binlog日之后,會將binlog日志先寫入到本地的relaylog中,relaylog中就保存了master的binlog日志。

最后,slave節(jié)點上的SQL線程會讀取relaylog中的biinlog日志,將其解析成具體的增刪改操作,把這些在master節(jié)點上進行過的操作,重新在slave節(jié)點上也重做一遍,打到數(shù)據(jù)還原的效果,這樣就可以保證master節(jié)點和slave節(jié)點的數(shù)據(jù)一致性了。

主從復制模式

MySQL的主從復制模式分為:全同步復制,異步復制,半同步復制,增強半同步復制。

全同步復制

全同步復制,就是當主庫執(zhí)行完一個事物之后,要求所有的從庫也都必須執(zhí)行完該事務,才可以返回處理結(jié)果給客戶端;因此雖然全同步復制數(shù)據(jù)一致性得到保證了,但是主庫完成一個事物需要等待所有從庫也完成,性能就比較低了。

異步復制

異步復制,當主庫提交事務后會通知binlog dump線程發(fā)送binlog日志給從庫,一旦binlog dump線程將binlog日志發(fā)送給從庫之后,不需要等到從庫也同步完成事務,主庫就會講處理結(jié)果返回給客戶端。

因為主庫只管自己執(zhí)行完事務,就可以將處理結(jié)果返回給客戶端,而不用關(guān)系從庫是否執(zhí)行完事務,這就可能導致短暫的主從數(shù)據(jù)不一致的問題了,比如剛在主庫插入的數(shù)據(jù),如果馬上在從庫查詢就可能查詢不到。

當主庫提交食物后,如果宕機掛掉了,此時可能binlog還沒來得及同步給從庫,這時候如果為了回復故障切換主從節(jié)點的話,就會出現(xiàn)數(shù)據(jù)丟失的問題,所以異步復制雖然性能高,但數(shù)據(jù)一致性上是比較弱的。

MySQL默認采用的是異步復制模式。

半同步復制

半同步復制就是在同步復制和異步中做了折中選擇,我們可以結(jié)合著MySQL官網(wǎng)來看下是半同步和主從復制的過程。



當主庫提交事務后,至少還需要一個從庫返回接收到binlog日志,并成功寫入到relaylog的消息,這個的時候,主庫才會講處理結(jié)果返回給客戶端。

相比前兩種復制方式,半同步復制較好地兼顧了數(shù)據(jù)一致性以及性能損耗的問題。

同時,半同步復制也存在以下幾個問題:

半同步復制的性能,相比異步復制而言有所下降,因為需要等到等待至少一個從庫確認接收到binlog日志的響應,所以新能上是有所損耗的。

主庫等待從庫響應的最大時長我們是可以配置的,如果超過了我們配置的事件,半同步復制就會變成異步復制,那么,異步復制的問題同樣也就出現(xiàn)了。

在MySQL5.7.2之前的版本中,半同步復制存在幻讀問題。當主庫成功提交事務并處于等待從庫確認的過程中,這個時候,從庫都還沒來得及返回處理結(jié)果給客戶端,但因為主庫存儲引擎內(nèi)部已經(jīng)提交事務了,所以,其他客戶端是可以到主庫中讀到數(shù)據(jù)的。但是,如果下一秒主庫宕機,下次請求過來只能讀取從庫,因為從庫還沒有從主庫同步數(shù)據(jù),所以從庫中讀取不到這條數(shù)據(jù)了,和上一次讀取數(shù)據(jù)的結(jié)果相比,就造成了幻讀的現(xiàn)象。



增強半同步復制

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

主庫配置了參數(shù)rpl_semi_sync_master_wait_point=AFTER_SYNC后,主庫在存儲引擎提交事務前,必須先首都哦啊從庫數(shù)據(jù)同步完成的確認信息后,才能提交事務,以此來解決幻讀問題。



主從同步實戰(zhàn)

準備數(shù)據(jù)源

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

配置數(shù)據(jù)源






package com.mcyz.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 {
    /**
     * 主庫數(shù)據(jù)源信息
     */
    private Map<String, String> masters;
    /**
     * 從庫數(shù)據(jù)源信息
     */
    private List<Map<String, String>> slaves;

    @SneakyThrows
    @Bean
    public DataSource masterDataSource() {
        log.info("masters:{}", masters);
        if (CollectionUtils.isEmpty(masters)) {
            throw new Exception("主庫數(shù)據(jù)源不能為空");
        }
        return DruidDataSourceFactory.createDataSource(masters);
    }

    @SneakyThrows
    @Bean
    public List<DataSource> slaveDataSources() {
        if (CollectionUtils.isEmpty(slaves)) {
            throw new Exception("從庫數(shù)據(jù)源不能為空");
        }
        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);
    }
}

數(shù)據(jù)源上下文切換

package com.mcyz.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("設(shè)置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();
    }
}



數(shù)據(jù)源路由實現(xiàn)類

package com.mcyz.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("當前數(shù)據(jù)源為: {}", DataSourceContextHolder.getDataSourceType());
        return DataSourceContextHolder.getDataSourceType();
    }
}

數(shù)據(jù)源切換注解

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

動態(tài)數(shù)據(jù)源切換切面

package com.mcyz.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() + "清除數(shù)據(jù)源");
        DataSourceContextHolder.remove();
    }
 
    @Override
    public int getOrder() {
        return 0;
    }
}

總結(jié)

項目整合讀寫分離主要是通過收到注入數(shù)據(jù)源,并通過攔截器設(shè)置當前線程的數(shù)據(jù)源類型,需要使用數(shù)據(jù)源的地方會通過數(shù)據(jù)源路由器讀取當前線程的數(shù)據(jù)源類型后返回實際的數(shù)據(jù)源進行數(shù)據(jù)庫的操作。

歡迎大家進行觀點的探討和碰撞,各抒己見。如果你有疑問,也可以找我溝通和交流。

最后給讀者整理了一份BAT大廠面試真題,需要的后臺回復“八股文”即可獲取。



作者:碼出宇宙

歡迎關(guān)注微信公眾號 :碼出宇宙

掃描添加好友邀你進技術(shù)交流群,加我時注明【姓名+公司(學校)+職位】