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é)校)+職位】