你還不知道怎么做數(shù)據(jù)庫讀寫分離么,用這個中間件讓你性能提10倍
前 言
訂單緩存方案上線之后,我們以為又開啟了歲月安好的日子,但是,在一周后的某一天,DBA直接跑來了,DBA直接說:“l(fā)eader讓我直接找你,是這樣的,上次加了緩存優(yōu)化后,效果確實(shí)不錯,但是我發(fā)現(xiàn)訂單查詢sql在今天的12:00至12:05之間有大量的慢sql,查詢時(shí)間超過了2.5s?!?/p>
這個時(shí)候,我們立馬開啟了排查問題模式,首先,check了一下上次加的緩存,發(fā)現(xiàn)緩存正常,然后接著根據(jù)DBA提供的信息搜索日志,此時(shí),發(fā)現(xiàn)在這個時(shí)間段訂單請求量突增,大概是平常訂單請求量的2到3倍,然后經(jīng)過了解,發(fā)現(xiàn)在這個時(shí)間段內(nèi),營銷系統(tǒng)那邊做了一些活動,導(dǎo)致訂單請求量突增。
說白了就是做了促銷活動后,大量下單的用戶會不斷刷新訂單來查詢訂單的信息,比如看一下訂單是否開始配送,此時(shí)大量的請求會打到了MySQL上去,此時(shí)單庫又抗不了這么讀請求,就導(dǎo)致了數(shù)據(jù)庫負(fù)載很高,從而嚴(yán)重降低了MySQL的查詢效率。
現(xiàn)在我們緩存也加過了,但是數(shù)據(jù)庫負(fù)載還是很高,此時(shí)該怎么辦呢?
其實(shí)也很簡單,既然單個庫扛不住,那就搞2個庫一起來抗唄,因?yàn)閷τ谕赓u訂單來說是典型的讀多寫少的場景,所以,在這個場景下,我們可以搞個一主兩從的架構(gòu)來進(jìn)行優(yōu)化,就像這樣:
也就是寫數(shù)據(jù)走主庫,而讀數(shù)據(jù)走從庫,可以看到,此時(shí)由于我們搞了2個從庫,這2個從庫可以一起來抗大量的讀請求。
非常關(guān)鍵的一點(diǎn)就是,從庫會通過主從復(fù)制,從主庫中不斷的同步數(shù)據(jù),以此來保證從庫的數(shù)據(jù)和主庫是一模一樣的,所以想要實(shí)現(xiàn)讀寫分離,那么,就先要了解主從復(fù)制是怎么玩兒的。
主從復(fù)制的原理是什么?
我們以mysql一主兩從架構(gòu)為例,也就是一個master節(jié)點(diǎn)下有兩個slave節(jié)點(diǎn),在這套架構(gòu)下,寫請求統(tǒng)一交給master節(jié)點(diǎn)處理,而讀請求交給slave節(jié)點(diǎn)處理。
為了保證slave節(jié)點(diǎn)和master節(jié)點(diǎn)的數(shù)據(jù)一致性,master節(jié)點(diǎn)在寫入數(shù)據(jù)之后,同時(shí)會把數(shù)據(jù)復(fù)制一份到自己的各個slave節(jié)點(diǎn)上。
在復(fù)制的過程中一共會使用到三個線程,一個是binlog dump線程,位于master節(jié)點(diǎn)上,另外兩個線程分別是I/O線程和SQL線程,它們都分別位于slave節(jié)點(diǎn)上,如下圖:
結(jié)合圖片,我們一起來看下主從復(fù)制的核心流程:
(1)當(dāng)master節(jié)點(diǎn)接收到一個寫請求時(shí),這個寫請求可能是增刪改操作,此時(shí)會把寫請求的操作都記錄到binlog日志中。
(2)master節(jié)點(diǎn)會把數(shù)據(jù)復(fù)制給slave節(jié)點(diǎn),如圖中的slave01節(jié)點(diǎn)和slave02節(jié)點(diǎn),這個過程,首先得要每個slave節(jié)點(diǎn)連接到master節(jié)點(diǎn)上,當(dāng)slave節(jié)點(diǎn)連接到master節(jié)點(diǎn)上時(shí),master節(jié)點(diǎn)會為每一個slave節(jié)點(diǎn)分別創(chuàng)建一個binlog dump線程,用于向各個slave節(jié)點(diǎn)發(fā)送binlog日志。
(3)binlog dump線程會讀取master節(jié)點(diǎn)上的binlog日志,然后將binlog日志發(fā)送給slave節(jié)點(diǎn)上的I/O線程。
(4)slave節(jié)點(diǎn)上的I/O線程接收到binlog日志后,會將binlog日志先寫入到本地的relaylog中,relaylog中就保存了binlog日志。
(5)slave節(jié)點(diǎn)上的SQL線程,會來讀取relaylog中的binlog日志,將其解析成具體的增刪改操作,把這些在master節(jié)點(diǎn)上進(jìn)行過的操作,重新在slave節(jié)點(diǎn)上也重做一遍,達(dá)到數(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)主庫執(zhí)行完一個事務(wù)之后,要求所有的從庫也都必須執(zhí)行完該事務(wù),才可以返回處理結(jié)果給客戶端;因此,雖然全同步復(fù)制數(shù)據(jù)一致性得到保證了,但是主庫完成一個事物需要等待所有從庫也完成,性能就比較低了。
異步復(fù)制
而異步復(fù)制,當(dāng)主庫提交事物后,會通知binlog dump線程發(fā)送binlog日志給從庫,一旦binlog dump線程將binlog日志發(fā)送給從庫之后,不需要等到從庫也同步完成事務(wù),主庫就會將處理結(jié)果返回給客戶端。
因?yàn)橹鲙熘还茏约簣?zhí)行完事務(wù),就可以將處理結(jié)果返回給客戶端,而不用關(guān)心從庫是否執(zhí)行完事務(wù),這就可能導(dǎo)致短暫的主從數(shù)據(jù)不一致的問題了,比如剛在主庫插入的新數(shù)據(jù),如果馬上在從庫查詢,就可能查詢不到。
而且,當(dāng)主庫提交事物后,如果宕機(jī)掛掉了,此時(shí)可能binlog還沒來得及同步給從庫,這時(shí)候如果為了恢復(fù)故障切換主從節(jié)點(diǎn)的話,就會出現(xiàn)數(shù)據(jù)丟失的問題,所以異步復(fù)制雖然性能高,但數(shù)據(jù)一致性上是較弱的。
mysql主從復(fù)制,默認(rèn)采用的就是異步復(fù)制這種復(fù)制策略。
半同步復(fù)制
半同步復(fù)制,顧名思義就是在同步和異步中做了折中選擇,我們可以結(jié)合著MySQL官網(wǎng)來看下是半同步主從復(fù)制的過程,來看下這樣圖:
當(dāng)主庫提交事務(wù)后,至少還需要一個從庫返回接受到binlog日志,并成功寫入到relaylog的消息,這個時(shí)候,主庫才會將處理結(jié)果返回給客戶端。
相比前2種復(fù)制方式,半同步復(fù)制較好地兼顧了數(shù)據(jù)一致性以及性能損耗的問題。
同時(shí),半同步復(fù)制也存在以下幾個問題:
- 半同步復(fù)制的性能,相比異步復(fù)制而言有所下降,相比于異步復(fù)制是不需要等待任何從庫是否接收到數(shù)據(jù)的響應(yīng),而半同步復(fù)制則需要等待至少一個從庫確認(rèn)接收到binlog日志的響應(yīng),性能上是損耗更大的。
- 主庫等待從庫響應(yīng)的最大時(shí)長是可以配置的,如果超過了配置的時(shí)間,半同步復(fù)制就會變成異步復(fù)制,那么,異步復(fù)制的問題同樣也就會出現(xiàn)了。
- 在MySQL 5.7.2之前的版本中,半同步復(fù)制存在著幻讀問題的。
當(dāng)主庫成功提交事物并處于等待從庫確認(rèn)的過程中,這個時(shí)候,從庫都還沒來得及返回處理結(jié)果給客戶端,但因?yàn)橹鲙齑鎯σ鎯?nèi)部已經(jīng)提交事務(wù)了,所以,其他客戶端是可以到從主庫中讀到數(shù)據(jù)的。
但是,如果下一秒主庫突然掛了,就像這樣圖一樣:
此時(shí),下一次請求過來,因?yàn)橹鲙鞉炝?,就只能把請求切換到從庫中,因?yàn)閺膸爝€沒從主庫同步完數(shù)據(jù),所以,從庫中當(dāng)然就讀不到這條數(shù)據(jù)了,和上一秒讀取數(shù)據(jù)的結(jié)果對比,就造成了幻讀的現(xiàn)象了。
增強(qiáng)半同步復(fù)制
最后,增強(qiáng)半同步復(fù)制,是mysql 5.7.2后的版本對半同步復(fù)制做的一個改進(jìn),原理上幾乎是一樣的,主要是解決幻讀的問題。
主庫配置了參數(shù)rpl_semi_sync_master_wait_point = AFTER_SYNC 后,主庫在存儲引擎提交事物前,必須先收到從庫數(shù)據(jù)同步完成的確認(rèn)信息后,才能提交事務(wù),以此來解決幻讀問題。
可以參考下MySQL官網(wǎng)是怎么描述增強(qiáng)半同步主從復(fù)制過程的:
主從延遲問題和常規(guī)解決方案
主庫寫入的速度是很快的,因?yàn)橹鲙焓嵌嗑€程并發(fā)寫入的,但是,從庫是單線程從主庫拉取數(shù)據(jù)的,所以從庫從主庫復(fù)制數(shù)據(jù)的速度,就比較慢了,從而產(chǎn)生了主從延遲的問題。
mysql 從 5.6版本開始,就支持多線程復(fù)制,但是5.6版本是基于庫級別去操作,也就是說會給每個數(shù)據(jù)庫開啟一個線程,不同庫處理時(shí)在同一時(shí)間內(nèi)是互不影響的;但是,當(dāng)業(yè)務(wù)的壓力集中到一個庫時(shí),又會回到和單線程復(fù)制一樣的狀況了。
直到mysql 5.7版本,開始引入了基于組提交(group_commit)的概念,這個時(shí)候才 真正 支持多路復(fù)制功能,官方稱為enhanced multi-threaded slave(簡稱MTS),所以,推薦大家盡可能選擇MySQL 5.7之后的版本。
而主庫掛載的從庫數(shù)量過多,也會導(dǎo)致主從復(fù)制延遲的問題,一般我們是建議一個主庫掛載從庫的數(shù)量,在3~5個比較合適。
另外,我們執(zhí)行的SQL語句中,如果慢SQL語句過多,也會導(dǎo)致主從復(fù)制延遲,比如,我們工作中會遇到批量插入的場景,如果一批插入的數(shù)據(jù)量過大,就容易造成執(zhí)行時(shí)間過長。
假如,從執(zhí)行完一份 批量插入數(shù)據(jù)的SQL語句開始,到在從庫上能查到這些數(shù)據(jù)的這個過程中,如果耗費(fèi)了10秒,就導(dǎo)致主從庫之間就延遲10秒了;所以,SQL優(yōu)化會是一個常態(tài)化的工作,可以通過慢SQL日志或監(jiān)控平臺監(jiān)控慢SQL,如果單個數(shù)據(jù)寫入時(shí)間過長的話,可以將一批數(shù)據(jù)分片分批次寫入。
最后,如果出現(xiàn)網(wǎng)絡(luò)延遲或者機(jī)器的性能比較差,也會導(dǎo)致主從復(fù)制延遲的問題,這種情況沒什么可說的,及時(shí)優(yōu)化網(wǎng)絡(luò)提升機(jī)器性能就行了。
讀寫分離實(shí)戰(zhàn)
讀寫分離配置核心組件流程圖:
讀寫分離配置步驟
(1)配置文件中配置主從庫連接信息
(2)注入數(shù)據(jù)源
(3)數(shù)據(jù)源切換上下文,其中使用了ThreadLocal保存當(dāng)前線程的數(shù)據(jù)源
(4)繼承AbstractRoutingDataSource類重寫determineCurrentLookupKey方法實(shí)現(xiàn)數(shù)據(jù)源動態(tài)切換
(5)創(chuàng)建讀庫的自定義注解
(6)切面類
(7)需要走讀庫的業(yè)務(wù)方法上添加@ReadOnly注解,那么執(zhí)行這些業(yè)務(wù)方法時(shí)就會被切面攔截修改數(shù)據(jù)源從而走讀庫進(jìn)行查詢。
(8)寫主庫、讀從庫的效果
1)生成訂單
2)查詢訂單