MySQL主從復(fù)制原理和使用

概述

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

主從復(fù)制原理

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

為了保證master節(jié)點(diǎn)和slave節(jié)點(diǎn)數(shù)據(jù)一致,在master節(jié)點(diǎn)寫(xiě)入數(shù)據(jù)后,會(huì)同時(shí)將數(shù)據(jù)復(fù)制到對(duì)應(yīng)的slave節(jié)點(diǎn)。主從復(fù)制數(shù)據(jù)的過(guò)程中會(huì)用到三個(gè)線程,master節(jié)點(diǎn)上的binlog dump線程,slave節(jié)點(diǎn)的I\O線程和SQL線程。



主從復(fù)制的核心流程:

當(dāng)master節(jié)點(diǎn)接收到一個(gè)寫(xiě)請(qǐng)求時(shí),這個(gè)寫(xiě)請(qǐng)求可能是增刪改操作,此時(shí)會(huì)把寫(xiě)請(qǐng)求的操作都記錄到binlog日志中。

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

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

slave幾點(diǎn)上的I/O線程接收到binlog日之后,會(huì)將binlog日志先寫(xiě)入到本地的relaylog中,relaylog中就保存了master的binlog日志。

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

主從復(fù)制模式

MySQL的主從復(fù)制模式分為:全同步復(fù)制,異步復(fù)制,半同步復(fù)制,增強(qiáng)半同步復(fù)制。

全同步復(fù)制

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

異步復(fù)制

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

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

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

MySQL默認(rèn)采用的是異步復(fù)制模式。

半同步復(fù)制

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



當(dāng)主庫(kù)提交事務(wù)后,至少還需要一個(gè)從庫(kù)返回接收到binlog日志,并成功寫(xiě)入到relaylog的消息,這個(gè)的時(shí)候,主庫(kù)才會(huì)講處理結(jié)果返回給客戶端。

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

同時(shí),半同步復(fù)制也存在以下幾個(gè)問(wèn)題:

半同步復(fù)制的性能,相比異步復(fù)制而言有所下降,因?yàn)樾枰鹊降却辽僖粋€(gè)從庫(kù)確認(rèn)接收到binlog日志的響應(yīng),所以新能上是有所損耗的。

主庫(kù)等待從庫(kù)響應(yīng)的最大時(shí)長(zhǎng)我們是可以配置的,如果超過(guò)了我們配置的事件,半同步復(fù)制就會(huì)變成異步復(fù)制,那么,異步復(fù)制的問(wèn)題同樣也就出現(xiàn)了。

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



增強(qiáng)半同步復(fù)制

增強(qiáng)半同步復(fù)制是MySQL5.7.2后的版本對(duì)半同步復(fù)制做的一個(gè)改進(jìn),原理幾乎是一樣的,主要是解決幻讀的問(wèn)題。

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



主從同步實(shí)戰(zhàn)

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

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

    @SneakyThrows
    @Bean
    public List<DataSource> slaveDataSources() {
        if (CollectionUtils.isEmpty(slaves)) {
            throw new Exception("從庫(kù)數(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ù)源路由實(shí)現(xiàn)類(lèi)

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("當(dāng)前數(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;
}

動(dòng)態(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() + "走從庫(kù)");
        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é)

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

歡迎大家進(jìn)行觀點(diǎn)的探討和碰撞,各抒己見(jiàn)。如果你有疑問(wèn),也可以找我溝通和交流。

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



作者:碼出宇宙

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

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