MySQL主從復(fù)制的原理圖解及Java語言示例使用
概述
實際生產(chǎn)的過程中為了實現(xiàn)數(shù)據(jù)庫的高可用,不會只有一個數(shù)據(jù)庫節(jié)點。至少會搭建主從復(fù)制的數(shù)據(jù)庫架構(gòu),從庫可以作為主庫的數(shù)據(jù)備份,以免主數(shù)據(jù)庫損壞的情況下丟失數(shù)據(jù);當(dāng)訪問量增加的時候可以作為讀節(jié)點承擔(dān)部分流量等。下面就進行從零開始搭建MySQL的主從架構(gòu)。
主從復(fù)制原理
以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ù)復(fù)制到對應(yīng)的slave節(jié)點。主從復(fù)制數(shù)據(jù)的過程中會用到三個線程,master節(jié)點上的binlog dump線程,slave節(jié)點的I\O線程和SQL線程。
主從復(fù)制的核心流程:
- 當(dāng)master節(jié)點接收到一個寫請求時,這個寫請求可能是增刪改操作,此時會把寫請求的操作都記錄到binlog日志中。
- master節(jié)點會把數(shù)據(jù)賦值給slave節(jié)點,如圖中的兩個slave節(jié)點。這個過程首先得要每個slave節(jié)點連接到master節(jié)點上,當(dāng)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ù)一致性了。
主從復(fù)制模式
MySQL的主從復(fù)制模式分為:全同步復(fù)制,異步復(fù)制,半同步復(fù)制,增強半同步復(fù)制。
全同步復(fù)制
全同步復(fù)制,就是當(dāng)主庫執(zhí)行完一個事物之后,要求所有的從庫也都必須執(zhí)行完該事務(wù),才可以返回處理結(jié)果給客戶端;因此雖然全同步復(fù)制數(shù)據(jù)一致性得到保證了,但是主庫完成一個事物需要等待所有從庫也完成,性能就比較低了。
異步復(fù)制
異步復(fù)制,當(dāng)主庫提交事務(wù)后會通知binlog dump線程發(fā)送binlog日志給從庫,一旦binlog dump線程將binlog日志發(fā)送給從庫之后,不需要等到從庫也同步完成事務(wù),主庫就會講處理結(jié)果返回給客戶端。
因為主庫只管自己執(zhí)行完事務(wù),就可以將處理結(jié)果返回給客戶端,而不用關(guān)系從庫是否執(zhí)行完事務(wù),這就可能導(dǎo)致短暫的主從數(shù)據(jù)不一致的問題了,比如剛在主庫插入的數(shù)據(jù),如果馬上在從庫查詢就可能查詢不到。
當(dāng)主庫提交食物后,如果宕機掛掉了,此時可能binlog還沒來得及同步給從庫,這時候如果為了回復(fù)故障切換主從節(jié)點的話,就會出現(xiàn)數(shù)據(jù)丟失的問題,所以異步復(fù)制雖然性能高,但數(shù)據(jù)一致性上是比較弱的。
MySQL默認(rèn)采用的是異步復(fù)制模式。
半同步復(fù)制
半同步復(fù)制就是在同步復(fù)制和異步中做了折中選擇,我們可以結(jié)合著MySQL官網(wǎng)來看下是半同步和主從復(fù)制的過程。
當(dāng)主庫提交事務(wù)后,至少還需要一個從庫返回接收到binlog日志,并成功寫入到relaylog的消息,這個的時候,主庫才會講處理結(jié)果返回給客戶端。
相比前兩種復(fù)制方式,半同步復(fù)制較好地兼顧了數(shù)據(jù)一致性以及性能損耗的問題。
同時,半同步復(fù)制也存在以下幾個問題:
- 半同步復(fù)制的性能,相比異步復(fù)制而言有所下降,因為需要等到等待至少一個從庫確認(rèn)接收到binlog日志的響應(yīng),所以新能上是有所損耗的。
- 主庫等待從庫響應(yīng)的最大時長我們是可以配置的,如果超過了我們配置的事件,半同步復(fù)制就會變成異步復(fù)制,那么,異步復(fù)制的問題同樣也就出現(xiàn)了。
- 在MySQL5.7.2之前的版本中,半同步復(fù)制存在幻讀問題。當(dāng)主庫成功提交事務(wù)并處于等待從庫確認(rèn)的過程中,這個時候,從庫都還沒來得及返回處理結(jié)果給客戶端,但因為主庫存儲引擎內(nèi)部已經(jīng)提交事務(wù)了,所以,其他客戶端是可以到主庫中讀到數(shù)據(jù)的。但是,如果下一秒主庫宕機,下次請求過來只能讀取從庫,因為從庫還沒有從主庫同步數(shù)據(jù),所以從庫中讀取不到這條數(shù)據(jù)了,和上一次讀取數(shù)據(jù)的結(jié)果相比,就造成了幻讀的現(xiàn)象。
增強半同步復(fù)制
增強半同步復(fù)制是MySQL5.7.2后的版本對半同步復(fù)制做的一個改進,原理幾乎是一樣的,主要是解決幻讀的問題。
主庫配置了參數(shù)rpl_semi_sync_master_wait_point=AFTER_SYNC后,主庫在存儲引擎提交事務(wù)前,必須先首都哦啊從庫數(shù)據(jù)同步完成的確認(rèn)信息后,才能提交事務(wù),以此來解決幻讀問題。
主從同步實戰(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.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 { /** * 主庫數(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.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("設(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.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("當(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; }
動態(tài)數(shù)據(jù)源切換切面
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() + "清除數(shù)據(jù)源"); DataSourceContextHolder.remove(); } @Override public int getOrder() { return 0; } }
總結(jié)
項目整合讀寫分離主要是通過收到注入數(shù)據(jù)源,并通過攔截器設(shè)置當(dāng)前線程的數(shù)據(jù)源類型,需要使用數(shù)據(jù)源的地方會通過數(shù)據(jù)源路由器讀取當(dāng)前線程的數(shù)據(jù)源類型后返回實際的數(shù)據(jù)源進行數(shù)據(jù)庫的操作。
到此這篇關(guān)于Java MySQL主從復(fù)制的原理圖解及示例使用的文章就介紹到這了,更多相關(guān)Java MySQL主從復(fù)制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
MySQL性能優(yōu)化之max_connections配置參數(shù)淺析
這篇文章主要介紹了MySQL性能優(yōu)化之max_connections配置參數(shù)淺析,本文著重講解了3種配置max_connections參數(shù)的方法,需要的朋友可以參考下2014-07-07MySQL Semisynchronous Replication介紹
這篇文章主要介紹了MySQL Semisynchronous Replication介紹,本文講解了Semisynchronous Replication 定義、,需要的朋友可以參考下2015-05-05MySQL 5.7.29 + Win64 解壓版 安裝教程圖文詳解
這篇文章主要介紹了MySQL 5.7.29 + Win64 解壓版 安裝教程,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-05-05