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