系列:MySQL 讀寫分離實(shí)現(xiàn)方案、主從復(fù)制原理與主從同步延時問題全解)
advanced-java 高并發(fā)系列MySQL 讀寫分離實(shí)現(xiàn)方案、主從復(fù)制原理與主從同步延時問題全解【免費(fèi)下載鏈接】advanced-java Core Interview Questions Answers For Experienced Java(Backend) Developers | 互聯(lián)網(wǎng) Java 工程師進(jìn)階知識完全掃盲涵蓋高并發(fā)、分布式、高可用、微服務(wù)、海量數(shù)據(jù)處理等領(lǐng)域知識項目地址: https://gitcode.com/gh_mirrors/ad/advanced-java導(dǎo)讀互聯(lián)網(wǎng)業(yè)務(wù)普遍是讀多寫少高并發(fā)架構(gòu)下必須借助 MySQL 主從復(fù)制搭建一主多從的讀寫分離架構(gòu)用多個從庫承接讀流量。本篇文章基于 advanced-java 倉庫高并發(fā)專題系統(tǒng)講解讀寫分離的實(shí)現(xiàn)方式、binlog 主從復(fù)制原理、半同步復(fù)制與并行復(fù)制兩個核心機(jī)制并結(jié)合真實(shí)線上案例給出主從同步延時的排查命令與完整解決方案幫助你在面試與生產(chǎn)實(shí)踐中都能把這條鏈路講透。面試題讀寫分離這條鏈路你能答到什么程度在 advanced-java 的高并發(fā)專題中圍繞讀寫分離提出了這樣一組經(jīng)典面試題你們有沒有做 MySQL 讀寫分離如何實(shí)現(xiàn) MySQL 的讀寫分離MySQL 主從復(fù)制原理是啥如何解決 MySQL 主從同步的延時問題這組問題層層遞進(jìn)先問有沒有做考察的是架構(gòu)實(shí)踐經(jīng)驗(yàn)再問原理是什么考察的是對復(fù)制鏈路底層機(jī)制的理解最后問延時怎么解考察的是遇到真實(shí)線上事故時的排查與解決能力。本文就沿著這條脈絡(luò)逐一展開。為什么高并發(fā)階段必須做讀寫分離先回答為什么。實(shí)際上絕大部分互聯(lián)網(wǎng)公司、網(wǎng)站或 App 的業(yè)務(wù)流量都是讀多寫少用戶瀏覽、搜索、列表展示的請求量遠(yuǎn)大于下單、改資料等寫入量。既然讀是主要矛盾就不能讓所有讀請求都壓在同一個庫上。讀寫分離的做法非常直接寫一個主庫主庫下面掛多個從庫所有寫請求只走主庫讀請求分散到多個從庫上從而用橫向擴(kuò)容從庫的方式支撐更高的讀并發(fā)壓力。這套思路在 advanced-java 的高并發(fā)系統(tǒng)設(shè)計中被明確列為扛住高并發(fā)的四大手段之一緩存、MQ、讀寫分離、分庫分表大部分時候數(shù)據(jù)庫可能也是讀多寫少沒必要所有請求都集中在一個庫上可以搞個主從架構(gòu)主庫寫入從庫讀取搞一個讀寫分離。讀流量太多的時候還可以加更多的從庫。同系列中 Redis 主從架構(gòu)也遵循同樣的套路——主從架構(gòu) → 讀寫分離 → 水平擴(kuò)容支撐讀高并發(fā)可見讀多寫少 主從復(fù)制 讀寫分離是緩存與數(shù)據(jù)庫兩個層面通用的高并發(fā)范式。如何實(shí)現(xiàn) MySQL 的讀寫分離實(shí)現(xiàn)本身并不復(fù)雜基于主從復(fù)制架構(gòu)搞一個主庫掛多個從庫業(yè)務(wù)代碼只寫主庫主庫自動把數(shù)據(jù)同步到從庫。搭建要點(diǎn)可以概括為三步配置主庫開啟log-bin二進(jìn)制日志并設(shè)置唯一的server-id主庫的所有變更都會記錄進(jìn) binlog配置從庫設(shè)置唯一的server-id通過CHANGE MASTER TO指定主庫地址、復(fù)制用戶與日志位點(diǎn)然后執(zhí)行START SLAVE啟動復(fù)制應(yīng)用層路由業(yè)務(wù)側(cè)將寫操作路由到主庫將讀操作路由到從庫。其中第 3 步在工程上通常有兩種落地方式應(yīng)用層封裝在 DAO 層或中間件層根據(jù) SQL 類型INSERT/UPDATE/DELETE走主庫SELECT走從庫做路由實(shí)現(xiàn)簡單可控數(shù)據(jù)庫中間件接入讀寫分離代理。advanced-java 的分庫分表專題中對這類中間件做過梳理例如淘寶 TDDL 屬于 client 層方案支持基本的 CRUD 語法和讀寫分離ShardingSphere 同時提供 client 層Sharding-JDBC與 proxy 層Sharding-Proxy方案支持分庫分表、讀寫分離、分布式 id 生成等能力。MySQL 主從復(fù)制原理binlog relay log 雙線程接力讀寫分離的地基是主從復(fù)制其核心流程如下對應(yīng)倉庫圖片 mysql-master-slave.png主庫記錄 binlog主庫將每一次數(shù)據(jù)變更寫入 binlog 二進(jìn)制日志從庫 IO 線程拉日志從庫連接到主庫后從庫上的 IO 線程把主庫的 binlog 日志拷貝到本地寫入relay log中繼日志從庫 SQL 線程重放從庫上的 SQL 線程從中繼日志讀取 binlog 內(nèi)容在自己本地再執(zhí)行一遍同樣的 SQL從而保證從庫數(shù)據(jù)與主庫一致。整個過程可以理解為日志接力主庫的變更先進(jìn) binlog經(jīng)過 IO 線程搬運(yùn)到 relay log再由 SQL 線程落地到從庫數(shù)據(jù)文件。關(guān)鍵點(diǎn)從庫同步是串行化的延時不可避免這里有一個非常重要、也最容易被忽視的點(diǎn)從庫同步主庫數(shù)據(jù)的過程是串行化的——主庫上并行的操作在從庫上會串行執(zhí)行。由于從庫從主庫拷貝日志 串行執(zhí)行 SQL這兩個特點(diǎn)在高并發(fā)場景下從庫的數(shù)據(jù)一定會比主庫慢一些是存在延時的。所以經(jīng)常出現(xiàn)這樣的現(xiàn)象剛寫入主庫的數(shù)據(jù)可能讀不到要過幾十毫秒甚至幾百毫秒才能讀到。更嚴(yán)重的是連帶風(fēng)險如果主庫突然宕機(jī)而數(shù)據(jù)恰好還沒同步到從庫那么這些數(shù)據(jù)在從庫上可能缺失造成數(shù)據(jù)丟失。兩大兜底機(jī)制半同步復(fù)制與并行復(fù)制針對上述兩個問題MySQL 提供了兩個對應(yīng)機(jī)制半同步復(fù)制semi-sync解決主庫數(shù)據(jù)丟失所謂半同步復(fù)制semi-sync指的是主庫寫入 binlog 日志之后強(qiáng)制立即將數(shù)據(jù)同步到從庫從庫把日志寫入自己本地的 relay log 之后會返回一個 ack 給主庫主庫接收到至少一個從庫的 ack 之后才會認(rèn)為這次寫操作完成。與異步復(fù)制的區(qū)別在于異步復(fù)制下主庫寫完 binlog 就立刻返回成功從庫是否收到、何時重放都不管半同步復(fù)制則引入至少一個從庫確認(rèn)的屏障從機(jī)制上堵住了主庫宕機(jī)、數(shù)據(jù)未同步就丟失的漏洞代價是寫路徑上多了一次從庫確認(rèn)的往返延遲。并行復(fù)制解決主從同步延時所謂并行復(fù)制指的是從庫開啟多個線程并行讀取 relay log 中不同庫的日志然后并行重放不同庫的日志——這是庫級別的并行。它針對的正是前面說的串行執(zhí)行 SQL瓶頸如果多個庫之間互不干擾那么把不同庫的日志分給不同線程并行重放從庫的追平速度就能大幅提升。注意它的粒度是庫如果寫并發(fā)全部集中在同一個庫上并行復(fù)制就無法發(fā)揮效果這一點(diǎn)在后面的延時解決方案里會再次體現(xiàn)。主從同步延時問題精華一次真實(shí)的線上事故復(fù)盤這一節(jié)是整個專題的精華部分源自 advanced-java 作者團(tuán)隊的真實(shí)線上經(jīng)歷曾經(jīng)因?yàn)橹鲝耐窖訒r問題導(dǎo)致線上 bug屬于小型生產(chǎn)事故。事故場景還原事故的代碼邏輯是這樣的先插入一條數(shù)據(jù)再把它查出來然后更新這條數(shù)據(jù)。生產(chǎn)環(huán)境高峰期寫并發(fā)達(dá)到了2000/s此時主從復(fù)制延時大概在小幾十毫秒線上每天總有那么一些數(shù)據(jù)期望更新重要的數(shù)據(jù)狀態(tài)但在高峰期卻沒有被更新用戶向客服反饋客服再反饋給開發(fā)團(tuán)隊。問題鏈條很清楚插入走主庫成功后緊接著的查詢被路由到了從庫而數(shù)據(jù)還沒同步過去查不到自然就無法執(zhí)行后面的更新邏輯導(dǎo)致重要狀態(tài)更新丟失。排查命令show slave status事故排查靠的是 MySQL 命令show slave status查看其中的Seconds_Behind_Master字段可以看到從庫復(fù)制主庫的數(shù)據(jù)落后了幾毫秒ms。該字段就是當(dāng)前主從延時的直接量化指標(biāo)也是日常監(jiān)控主從健康度的核心依據(jù)一旦Seconds_Behind_Master持續(xù)增大或長期不為 0就說明從庫追不上主庫的寫入速度需要介入處理。四種解決方案與取舍針對主從延遲一般有以下解決方案方案做法適用場景與取舍分庫將一個主庫拆分為多個主庫每個主庫的寫并發(fā)就減少幾倍從根上攤薄單主庫寫壓力此時主從延遲可以忽略不計代價是需要引入分庫改造開啟并行復(fù)制打開 MySQL 支持的并行復(fù)制多個庫并行復(fù)制適合寫并發(fā)分散在多個庫的場景如果某個庫的寫入并發(fā)就是特別高例如單庫寫并發(fā)達(dá)到 2000/s并行復(fù)制還是沒意義重寫代碼寫代碼的同學(xué)要慎重插入數(shù)據(jù)時立馬查詢可能查不到從業(yè)務(wù)邏輯上規(guī)避插入即查的時序依賴成本最低、最推薦查詢直連主庫如果確實(shí)存在必須先插入、立馬就要查到、隨后立即執(zhí)行操作的強(qiáng)一致需求對該查詢設(shè)置直連主庫不推薦一旦這么搞讀寫分離的意義就喪失了方案背后的本質(zhì)思考這四種方案其實(shí)對應(yīng)了三個層面的權(quán)衡架構(gòu)層面分庫是從減少單主庫寫并發(fā)入手讓復(fù)制壓力落在可控范圍內(nèi)數(shù)據(jù)庫層面并行復(fù)制是從提升從庫重放速度入手但受限于庫級并行的粒度應(yīng)用層面重寫代碼與直連主庫是從業(yè)務(wù)語義入手——要么消除插入即讀的強(qiáng)時序依賴要么用犧牲讀寫分離收益的方式換取強(qiáng)一致。生產(chǎn)環(huán)境中的正確姿勢通常是組合拳能用分庫降低單庫寫壓力的先分庫能改的業(yè)務(wù)邏輯優(yōu)先改成插入后不立即查詢把插入即查這類強(qiáng)一致場景收斂到極少數(shù)必須的場景而不是全局直連主庫。延伸從架構(gòu)全局看讀寫分離的邊界把讀寫分離放進(jìn) advanced-java 高并發(fā)體系的全局里看它的邊界與配套手段非常清晰讀多寫少才適合讀寫分離如果業(yè)務(wù)本身就是寫密集主從復(fù)制帶來的延時會持續(xù)放大這時候應(yīng)該考慮 分庫分表 與 MQ 削峰而不是單純堆從庫緩存是第一道防線高并發(fā)系統(tǒng)設(shè)計中緩存是抗讀并發(fā)的首選大量讀請求應(yīng)先被緩存吸收讀寫分離承接的是緩存未命中后的數(shù)據(jù)庫讀流量中間件是工程化抓手當(dāng)讀寫分離規(guī)則散落在多個業(yè)務(wù)團(tuán)隊時通過數(shù)據(jù)庫中間件統(tǒng)一路由如分庫分表專題中提到的 TDDL、ShardingSphere 等比每個應(yīng)用各自封裝更可控主從復(fù)制與 Redis 主從同源同理Redis 主從架構(gòu)展示了同一套主從 異步復(fù)制 讀寫分離 水平擴(kuò)容范式在緩存層的應(yīng)用兩者對照理解主從復(fù)制的通用心法異步復(fù)制有延時、節(jié)點(diǎn)擴(kuò)容扛讀就一通百通了。總結(jié)回到開頭的面試題完整的回答路徑是實(shí)現(xiàn)方案主從復(fù)制架構(gòu) 一主多從寫主庫、讀從庫應(yīng)用層或中間件層做讀寫路由復(fù)制原理主庫寫 binlog → 從庫 IO 線程拉取寫入 relay log → 從庫 SQL 線程串行重放從庫同步串行化決定了延時必然存在兩大機(jī)制半同步復(fù)制至少一個從庫 ack 后寫操作才算完成解決主庫宕機(jī)丟數(shù)據(jù)并行復(fù)制庫級別多線程并行重放緩解從庫同步延時延時排查與解決show slave status看Seconds_Behind_Master用分庫、并行復(fù)制、重寫代碼規(guī)避插入即查、必要時直連主庫不推薦來治理。推薦結(jié)合倉庫原文與相關(guān)專題繼續(xù)深入MySQL 讀寫分離原文、高并發(fā)系統(tǒng)設(shè)計、分庫分表方案、Redis 主從架構(gòu)?!久赓M(fèi)下載鏈接】advanced-java Core Interview Questions Answers For Experienced Java(Backend) Developers | 互聯(lián)網(wǎng) Java 工程師進(jìn)階知識完全掃盲涵蓋高并發(fā)、分布式、高可用、微服務(wù)、海量數(shù)據(jù)處理等領(lǐng)域知識項目地址: https://gitcode.com/gh_mirrors/ad/advanced-java創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考