架構與思維:高並發下解決主從延時的一些思路
1 回顧下MySQL主從複製
主從複製,是指建立一個和主資料庫完全一樣的資料庫環境(稱為從資料庫),並將主庫的操作行為進行複製的過程:將主資料庫的DDL和DML的操作日誌同步到從資料庫上,
然後在從資料庫上對這些日誌進行重新執行,來保證從資料庫和主資料庫的數據的一致性。
1.1 為什麼要做主從複製
1、在複雜的業務操作中,經常會有操作導致鎖行甚至鎖表的情況,如果讀寫不解耦,會很影響運行中的業務,使用主從複製,讓主庫負責寫,從庫負責讀。
即使主庫出現了鎖表的情景,通過讀從庫也可以保證業務的正常運行。
2、保證數據的熱備份,主庫宕機後能夠及時替換主庫,保障業務可用性。
3、架構的演進:業務量擴大,I/O訪問頻率增高,單機無法滿足,主從複製可以做多庫方案,降低磁碟I/O訪問的頻率,提高單機的I/O性能。
4、本質上也是分治理念,主從複製、讀寫分離即是壓力分拆的過程。
5、讀寫比也影響整個拆分方式,讀寫比越高,主從庫比例應越高,才能保證讀寫的均衡,才能保證較好的運行性能。讀寫比下的主從分配方法下:
讀寫比(大約) | 主庫 | 從庫 |
50:50 | 1 | 1 |
66.6:33.3 | 1 | 2 |
80:20 | 1 | 4 |
— — | — — | — — |
1.2 主從複製的原理
當在從庫上啟動複製時,首先創建I/O執行緒連接主庫,主庫隨後創建Binlog Dump執行緒讀取資料庫事件並發送給I/O執行緒,I/O執行緒獲取到事件數據後更新到從庫的中繼日誌Relay Log中去,之後從庫上的SQL執行緒讀取中繼日誌Relay Log中更新的資料庫事件並應用,
如下圖所示:
細化一下有如下幾個步驟:
1、MySQL主庫在事務提交時把數據變更(insert、delet、update)作為事件日誌記錄在二進位日誌表(binlog)裡面。
2、主庫上有一個工作執行緒 binlog dump thread,把binlog的內容發送到從庫的中繼日誌relay log中。
3、從庫根據中繼日誌relay log重做數據變更操作,通過邏輯複製來達到主庫和從庫的數據一致性。
4、MySQL通過三個執行緒來完成主從庫間的數據複製,其中binlog dump執行緒跑在主庫上,I/O執行緒和SQL執行緒跑在從庫上。擁有多個從庫的主庫會為每一個連接到主庫的從庫創建一個binlog dump執行緒。
1.3 主從延遲的原因
MySQL主從複製,讀寫分離是我們常用的資料庫架構,但是在並發量較大、數據變化大的場景下,主從延時會比較嚴重。
延遲的本質原因是:系統TPS並發較高時,主庫產生的DML(也包含一部分DDL)數量超過Slave一個Sql執行緒所能承受的範圍,效率就降低了。

2 幾種解決方案
2.1 最優的系統配置
優化系統配置(系統級、鏈接層、存儲引擎層),讓資料庫處在最優狀態:最大連接數、允許錯誤數、允許超時時間、pool_size、log_size等,保證記憶體、CPU、存儲空間的擴容(硬體部分)。

1 # TIME_WAIT超時時間,默認是60s 2 net.ipv4.tcp_fin_timeout = 30 3 # 增加tcp支援的隊列數,加大隊列長度可容納更多的等待連接 4 net.ipv4.tcp_max_syn_backlog = 65535 5 # 減少斷開連接時 ,資源回收 6 net.ipv4.tcp_max_tw_buckets = 8000 7 net.ipv4.tcp_tw_reuse = 1 8 net.ipv4.tcp_tw_recycle = 1 9 net.ipv4.tcp_fin_timeout = 10 10 # 打開文件的限制 11 *soft nofile 65535 12 *hard nofile 65535
公共參數默認值:
1 max_connections = 151 2 # 同時處理最大連接數,建議設置最大連接數是上限連接數的80%左右,一般默認值為151,可以做適當調整。 3 sort_buffer_size = 2M 4 # 查詢排序時緩衝區大小,只對order by和group by起作用,建議增大為16M 5 open_files_limit = 1024 6 # 打開文件數限制,如果show global status like 'open_files'查看的值等於或者大於open_files_limit值時,程式會無法連接資料庫或卡死
InnoDB參數默認值:
1 innodb_buffer_pool_size = 128M 2 # 索引和數據緩衝區大小,建議設置物理記憶體的70%左右(這個前提是這個伺服器只用做Mysql資料庫伺服器) 3 innodb_buffer_pool_instances = 1 4 # 緩衝池實例個數,推薦設置4個或8個 5 innodb_flush_log_at_trx_commit = 1 6 # 關鍵參數,0代表大約每秒寫入到日誌並同步到磁碟,資料庫故障會丟失1秒左右事務數據。1為每執行一條SQL後寫入到日誌並同步到磁碟,I/O開銷大,執行完SQL要等待日誌讀寫,效率低。2代表只把日誌寫入到系統快取區,再每秒同步到磁碟,效率很高,如果伺服器故障,才會丟失事務數據。對數據安全性要求不是很高的推薦設置2,性能高,修改後效果明顯。 7 sync_binlog=1 8 9 innodb_file_per_table = ON 10 # 是否共享表空間,5.7+版本默認ON,共享表空間idbdata文件不斷增大,影響一定的I/O性能。建議開啟獨立表空間模式,每個表的索引和數據都存在自己獨立的表空間中,可以實現單表在不同資料庫中移動。 11 innodb_log_buffer_size = 8M 12 # 日誌緩衝區大小,由於日誌最長每秒鐘刷新一次,所以一般不用超過16M
2.2 資料庫層做合理分治
資料庫分區是永恆的話題,主從延遲一定程度上是單台資料庫主服務操作過於頻繁,使得單執行緒的SQL thread 疲於應付。可以適當的從功能上對資料庫進行拆分,分擔壓力。
資料庫拆分可以參考我的這篇文章《分庫分表》,這邊就不贅述。
2.3 從庫同步完成後響應
假如你的業務時間允許,你可以在寫入主庫的時候,確保數據都同步到從庫了之後才返回這條數據寫入成功,當然如果有多個從庫,你也必須確保每個從庫都寫入成功。當然,這個方案對性能和時間的消耗是極大的,會直接降低你的系統吞吐量,不推薦。
2.4 適當引入快取
可以引入redis或者其他nosql資料庫來存儲我們經常會產生主從延遲的業務數據。當我在寫入資料庫的同時,我們再寫入一份到redis中。
讀取數據的時候,我們可以先去查看redis中是否有這個數據,如果有我們就可以直接從redis中讀取這個數據。當數據真正同步到資料庫中的時候,再從redis中把數據刪除。如下圖:
這邊還需注意兩點,很重要喲,面試必問:
1、雖然一定程度上緩解延遲的問題,但如果遇到高並發的情況,對Redis的頻繁刪除也不合理,所以需要結合場景綜合考慮,比如定期刪除快取。
2、高並發情況下可能存在slave還沒同步,又有新的值寫進來了,這時候Master –> Slave 還在排隊中,但是Cache已經被更新了。所以如果對Redis進行刪除,可能會誤刪除最新的快取值,導致讀取到的數據是舊的。
如上圖情況,對一個值分別更新 1,2,3,主從同步按照順序進行,剛同步完1,Cache就更新到3了,這時候如果把Cache刪除了,讀請求就會走到從庫去讀,讀到了1,數據就會出現短暫不一致了。
所以這個地方也需要注意,可以同時將唯一鍵(比如主鍵)也做保存,刪除之前做一個判斷,避免誤刪。或者乾脆不實時刪除快取,低峰值期再來處理。
2.5 多執行緒重放RelayLog
MySQL使用單執行緒重放RelayLog,那能不能在這上面做解法呢,比如使用多執行緒並行重放RelayLog,就可以縮短時間。但是這個對數據一致性是個考驗。
需要考慮如何分割RelayLog,才能夠讓多個資料庫實例,多個執行緒並行重放RelayLog,不會出現不一致。比如RelayLog包含這三條語句給學生授予學分的記錄,你就不知道結果會變成什麼。可能是806甚至是721。
1 update t_score set score = 721 where stu_code=374532; 2 update t_score set score = 806 where stu_code=374532; 3 update t_score set score = 899 where stu_code=374532;
解法就是:
相同庫表上的寫操作,用相同的執行緒來重放RelayLog;不同庫表上的寫操作,可以並發用多個執行緒並發來重放RelayLog。
設計一個哈希演算法,hash(db-name) % thread-num,表名稱hash之後再模上執行緒數,就能很輕易做到,同一個庫表上的寫操作,被同一個重放執行緒串列執行,做到提效的目的。
這其實也是一種分治的思維,類似上面直接對資料庫進行拆分。
2.6 少量讀業務直連主庫
2.7 適當的限流、降級
3 總結
上面提到了多種方案都是可以討論,每個方法都有弊端和優勢,根據實際情況進行選型。在面試的過程中,也經常遇到候選人跟我討論的。
另外,mysql5.6之後,可以按照庫並行複製。mysql5.7之後,提供了基於GTID並行複製的能力。可以參考學習下。