MySQL 讀寫分離:過期讀問題與解決方案全攻略
前言
現實商務環境中,MySQL 讀寫分離是非常常見的架構。但當你寫入資料後立即查詢,卻發現資料「消失」了?這就是經典的「⚠️ 過期讀」問題!本文將深入分析原因與解決方案。
讀寫分離文章重點快速理解一圖流
一圖流展開/收和
為什麼採用「一寫多讀」架構?
主流商業環境都採用「一寫多讀」而非「多寫多讀」架構,原因是什麼?
| 架構 | 說明 | 風險 |
|---|---|---|
| 一寫多讀 | 單一主庫負責寫入,多個從庫負責讀取 | 無資料衝突風險,但可能發生過期讀 |
| 多寫多讀 | 多個主庫都可寫入 | 當 Binlog 延遲時,除了過期讀,更會發生資料間的 PK 衝突(如兩邊同時新增 ID=10) |
結論:目前我自己實務經驗中,幾乎接觸到的都是一寫多讀居多,幾乎沒遇過多寫多讀的架構設計,所以這篇文章也主要 針對一寫多讀來進行詳細的技術討論。
(備註:未來如果有特殊案例使用多寫多讀的架構,我將在補充在這篇文章。)
讀寫分離的同步原理
在深入問題之前,先了解讀寫分離的同步流程(以下僅列重點流程,不會將完整的 InnoDB 下的 two-phase commit 都展開):
1 | Step 1. 主庫異動資料(執行新增、修改或刪除操作) |
問題:什麼情況會導致「過期讀」?
「過期讀」指的是:從庫的資料尚未與主庫同步,導致查詢到舊資料。
三大常見原因
| 原因 | 說明 |
|---|---|
| Big Transaction | 一次刪除百萬筆資料,主庫 Binlog 快速寫入,但從庫需要較長時間同步 |
| 高併發 | 主庫效能高、從庫較弱時,SQL Threads 需排隊處理 |
| 網路問題 | 跨區部署(如美國↔日本)會有網路延遲 |
監控指標: 在 AWS CloudWatch 可監控 ReplicaLag:
- 數值 = 0:完全同步
- 數值 = 10:從庫資料落後主庫 10 秒
解法一:緊急止血方案
需要快速上線、無法大改架構時的臨時解法。
強制走主庫
直接放棄讀寫分離,所有查詢都走主庫。
優點:最簡單、100% 保證資料一致性
缺點:失去讀寫分離的意義,主庫負擔加重
Sleep 等待
在查詢前加入 Sleep,等待從庫同步完成。
1 | // 寫入後等待 1 秒再查詢 |
優點:實作簡單
缺點:不精確、浪費時間、非主流做法
缺點:僅適合緊急情況,長期不建議使用
延伸:解決 Sleep 不精確的最佳解
Sleep 的問題在於「不精確」——延遲 400ms 時你等了 2 秒是浪費,延遲 2.5 秒時你等 2 秒又不夠。以下是更精確的做法。
查詢同步狀態
1 | -- MySQL 8.0.22+ |
三種判斷方式
| 方式 | 說明 | 精確度 |
|---|---|---|
seconds_behind_master | 查看是否為 0(單位:秒) | ⭐ 低 |
| 對比位點 | 比對 Master_Log_File 與 Relay_Master_Log_File | ⭐⭐ 中 |
| 對比 GTID | 比對 Retrieved_Gtid_Set 與 Executed_Gtid_Set | ⭐⭐⭐ 高 |
對比位點詳解
1 | Master_Log_File 和 Read_Master_Log_Pos:讀到的主庫最新位點 |
對比 GTID 詳解
1 | Auto_Position=1:表示使用 GTID 協議 |
推薦: 對比位點和 GTID 比判斷 seconds_behind_master 更精確。
解法二:MySQL 8.0+ 主流解法
MySQL 8.0.14+ 在 Group Replication (GR) 模式下提供 group_replication_consistency 參數,讓資料庫自動處理同步等待,無需應用層實作。注意:這適用於GR,Standard Source-Replica 架構是不支援以下參數。
(GR 要有設定啟用才可以,使用 「SELECT * FROM performance_schema.replication_group_members;」 來確認自己的 Mysql 是否有 GR。)
參數說明
| 參數值 | 行為 | 適用場景 |
|---|---|---|
EVENTUAL | (預設)不等待,直接執行 | 重視效能,可容忍舊資料 |
BEFORE_ON_PRIMARY_FAILOVER | 只在主庫切換時等待 | 防止切換時讀錯資料 |
BEFORE | 執行前等待此節點追上所有更新 | 從庫使用,確保不讀舊資料 |
AFTER | 寫入後等待其他節點套用完成 | 主庫使用,確保寫入後全局可見 |
從庫使用範例(BEFORE)
1 | -- 連線到從庫 |
主庫使用範例(AFTER)
1 | -- 連線到主庫 |
解法三:AWS Aurora 專用方案
使用 AWS Aurora 時,有專屬的解決方案(Aurora 不使用 group_replication_consistency)。
使用 replica_host_status 監控
查詢各從庫的延遲狀況,選擇延遲最低的從庫:
1 | SELECT * FROM information_schema.replica_host_status; |
策略:
- 選擇延遲最低的從庫查詢
- 若所有從庫延遲都很高,則直接查主庫
優點:毫秒級延遲監控(比傳統 MySQL 秒級更精確)
缺點:需要應用層實作選擇邏輯
使用 aurora_replica_read_consistency
名詞解釋: LSN (Log Sequence Number) 是日誌序列號,用於追蹤資料庫變更進度,Aurora 透過 LSN 確保從庫與主庫的同步狀態
1 | -- 設定強制全局一致性 |
優點:Aurora 原生支援,實作簡單
缺點:增加查詢延遲
進階議題:資料保護機制(斷電、特殊情況處理)
情境: 如果遭遇斷電或特殊狀況,導致主庫同步 Binlog 到從庫時出現意外中斷,資料遺失該怎麼辦?
重要: Semi-Sync 預設是關閉的,需要手動安裝 Plugin 並啟用!
Semi-Sync 半同步複製
確保 Binlog 傳到從庫後才回應 Client。
ACK 定義: ACK 是一種「確認訊號(acknowledgement)」,表示收到資料並成功處理過的回應。
執行流程:
1 | 主庫寫 Binlog |
如何開啟 Semi-Sync:
以下指令皆在 MySQL Client 中執行(透過 mysql -u root -p 連線後執行)。
1 | -- ========== 主庫設定(連線到主庫執行)========== |
檢查 Semi-Sync 狀態:
1 | -- 查看 Plugin 是否已安裝 |
關鍵狀態指標:
| 變數 | 說明 |
|---|---|
Rpl_semi_sync_master_status | ON 表示主庫 Semi-Sync 運行中 |
Rpl_semi_sync_slave_status | ON 表示從庫 Semi-Sync 運行中 |
Rpl_semi_sync_master_clients | 已連線的 Semi-Sync 從庫數量 |
永久生效: 上述設定重啟後會失效,若要永久生效需寫入 my.cnf:
1 | # 主庫 |
優點:有效防止資料丟失
缺點:增加寫入延遲
AFTER_SYNC 無損複製
MySQL 8.0 開啟 Semi-Sync 時,預設使用 AFTER_SYNC 模式(wait_point=AFTER_SYNC)。
MySQL 8.0+ 開啟方式(指令有變更):
1 | -- ========== 主庫設定 ========== |
名稱變更: MySQL 8.0 將 master/slave 改為 source/replica,舊名稱仍可用但已標記為 deprecated。
流程:
1 | 主庫寫 Binlog → 傳給從庫 → 等待 ACK → 主庫 Commit → 回覆客戶端 |
斷電場景:
- 主庫傳給從庫但還沒收到 ACK 時斷電
- 因為卡在「等待 ACK」,主庫尚未 Commit
- 重啟後該筆交易會被 Rollback
結果:主庫沒資料、從庫也沒資料,資料一致(雖然都失敗了)
注意: rpl_semi_sync_master_timeout 預設 10,000 ms。若從庫超時(例如從庫掛了),主庫開始瘋狂寫資料後也掛了,此時 Semi-Sync 等同失效,仍會造成資料遺失。
Group Replication
使用 Paxos-like 協議,需要多數節點同意才算真正 Commit。
流程:
1 | 1. 主庫寫入事件 |
斷電場景:
- 若所有節點未達成 quorum → 不算 commit → 沒人 commit 這筆 transaction
- 若已達 quorum → 即使主庫斷電,新主節點仍有這筆資料且一致
優點:即使主庫斷電,新主節點仍有資料
優點:支援自動故障轉移
缺點:架構較複雜
MySQL 5.7 以前的風險:AFTER_COMMIT
MySQL 5.7 以前只有 AFTER_COMMIT 模式,存在「Phantom Data」風險。
流程:
1 | 主庫寫 Binlog → 主庫 Commit(資料落地)→ 傳給從庫 → 等待 ACK → 回覆客戶端 |
悲劇場景: 主庫 Commit 了,正要傳給從庫時主庫掛了。
| 角色 | 結果 |
|---|---|
| 主庫 | 有這筆資料 |
| 從庫 | 沒有這筆資料 |
| 客戶端 | 收到超時/錯誤 |
風險:雖然客戶端知道失敗了,但資料已在舊主庫的硬碟裡(Phantom Data)。重啟舊主庫會造成數據衝突。
建議:升級 MySQL 版本到 8.0 以上,使用 AFTER_SYNC。
實戰場景與策略選擇
以下依照實作難度分類,幫助你快速找到適合的策略。
入門級:零改動或少量改動
直接走從庫(不做一致性處理)
| 項目 | 說明 |
|---|---|
| 場景 | 每日/週/月報表,每次需要產出數百萬筆的資料統計,查詢及產出需要耗時 3~5 分鐘 |
| 痛點 | 報表佔用大量資源,若在主庫執行會嚴重影響主庫性能,導致整體用戶體驗下降 |
| 策略 | 一律走從庫,不做任何一致性處理(使用 EVENTUAL) |
優點:不影響主庫效能、程式與架構都簡單實作
缺點:報表資料與即時資料可能會有落差,一般以「歷史資料」來產出
強制走主庫查詢
| 項目 | 說明 |
|---|---|
| 場景 | 用戶在電商平台完成付款後,系統跳轉到「我的訂單」頁面,用戶期望立即看到剛剛下的訂單及付款狀態 |
| 痛點 | 若從從庫讀取可能因同步延遲而顯示「無訂單」,用戶誤以為下單失敗而重複下單,造成客訴和重複扣款 |
| 策略 | 針對特定查詢強制從主庫讀取,不經過從庫 |
優點:精確控制哪些查詢需要強一致性;實作邏輯簡單;100% 保證讀到最新數據
缺點:需要判斷哪些場景需要強制主庫;過度使用會失去讀寫分離的意義;增加主庫負擔
Semi-Sync 半同步複製
| 項目 | 說明 |
|---|---|
| 場景 | 線上購物平台的訂單資料庫,用戶完成付款後系統回覆「訂單成立」,必須確保訂單資料已安全存放,即使主庫下一秒斷電也不能丟失 |
| 痛點 | 異步複製下主庫斷電,客戶端已收到「提交成功」但 Binlog 未傳輸到從庫,導致訂單資料永遠丟失 |
| 策略 | 啟用 Semi-Sync,主庫等待至少一個從庫確認收到 Binlog 後才回覆客戶端成功 |
優點:配置簡單;不需要改變應用程式;有效防止資料丟失;成本低
缺點:網路延遲高或從庫故障時可能退化為異步複製;不提供自動故障轉移;無法解決過期讀問題
進階級:需要應用層配合實作
Semi-Sync 搭配 GTID 等待
| 項目 | 說明 |
|---|---|
| 場景 | 用戶上傳商品圖片後,系統需要取得圖片 ID 並更新商品資料,但圖片記錄剛寫入主庫,從庫可能尚未同步完成 |
| 痛點 | Semi-Sync 只保證 Binlog 傳輸到從庫,但從庫可能尚未執行完成,查詢時仍會發生過期讀導致業務邏輯失敗 |
| 策略 | 寫入後記錄 GTID,讀取從庫前使用 WAIT_FOR_EXECUTED_GTID_SET() 等待從庫執行完成 |
優點:精確等待特定交易同步完成;可針對單一查詢控制;搭配 Semi-Sync 可同時解決資料丟失和過期讀
缺點:需要應用層配合實作等待邏輯;從庫延遲大時等待時間長;需要啟用 GTID 模式
Semi-Sync 搭配位點等待
| 項目 | 說明 |
|---|---|
| 場景 | 運行多年的舊版電商系統(MySQL 5.5),無法升級也無法啟用 GTID,但仍需解決用戶修改收貨地址後立即查詢卻看到舊地址的問題 |
| 痛點 | 沒有 GTID 的情況下,無法使用 GTID 等待機制,需要其他方式確保從庫已同步特定交易 |
| 策略 | 寫入後記錄 Binlog 檔案和位點,讀取從庫前使用 MASTER_POS_WAIT() 等待從庫執行到該位點 |
優點:不需要 GTID;適用於舊版 MySQL;原理與 GTID 等待相同
缺點:位點管理比 GTID 複雜(需記錄檔案名和位置);主從切換後位點會變化;應用層實作更繁瑣
Group Replication 一致性等級 BEFORE
| 項目 | 說明 |
|---|---|
| 場景 | 客服系統中,客服人員剛為用戶建立了退款工單,隨即點擊查看工單詳情,系統必須顯示剛建立的工單內容,而非回報「工單不存在」 |
| 痛點 | 從庫查詢時可能讀到過期數據,但不想所有查詢都走主庫增加負擔;手動實作 GTID 等待邏輯太複雜且容易出錯 |
| 策略 | 查詢前設定 group_replication_consistency = 'BEFORE',MySQL 自動等待從庫同步完成後再執行查詢 |
優點:MySQL 內部自動處理同步等待邏輯,無需應用層實作;可針對 Session 或單次查詢設定
缺點:需要 MySQL 8.0.14+ 且啟用 Group Replication;從庫延遲大時查詢會等待較久
高階級:需要架構調整或升級
Group Replication 一致性等級 AFTER
| 項目 | 說明 |
|---|---|
| 場景 | 企業內部系統的管理員修改了某用戶的存取權限(從一般用戶升級為 VIP),修改完成後該用戶無論連到哪個資料庫節點都必須立即擁有 VIP 權限 |
| 痛點 | 主庫更新完成後,連到其他從庫的用戶可能讀到舊的權限資料,造成權限判斷錯誤,用戶明明已升級卻無法使用 VIP 功能 |
| 策略 | 寫入時設定 group_replication_consistency = 'AFTER',交易會等待所有從庫套用完成後才回傳成功 |
優點:確保寫入後任何節點都能讀到最新數據;適合需要全局一致的配置類更新
缺點:寫入延遲明顯增加(需等待所有從庫套用);不適合高頻寫入場景
MySQL Router + Group Replication(MySQL 8.2+)
| 項目 | 說明 |
|---|---|
| 場景 | 大型企業的 ERP 系統,有數十個微服務連接資料庫,不希望在每個服務中都實作讀寫分離邏輯,也不想在主從切換時逐一修改各服務的資料庫連線設定 |
| 痛點 | 應用程式需要自行判斷讀寫並連接不同端點;主從切換時需要修改所有應用的配置並重新部署,動輒影響數十個服務 |
| 策略 | 使用 MySQL Router 作為代理層,自動識別 SQL 類型並路由到主庫或從庫,搭配 Group Replication 提供高可用 |
優點:應用程式只需連接單一端點;讀寫路由完全透明;主從切換自動處理無需改應用
缺點:多一層代理架構;需要額外維護 Router 服務;MySQL 8.2+ 才支援透明讀寫分離
AWS 雲端方案
AWS Aurora + replica_host_status 監控
| 項目 | 說明 |
|---|---|
| 場景 | 線上遊戲公司使用 Aurora 部署了 1 個 Writer 和 5 個 Reader,希望將玩家的查詢請求導向延遲最低的 Reader,以提供最佳的遊戲體驗 |
| 痛點 | Aurora Reader Endpoint 使用輪詢分配連線,可能將請求導向延遲較高的 Reader;無法像傳統 MySQL 用 seconds_behind_master 監控各節點延遲 |
| 策略 | 查詢 information_schema.replica_host_status 取得各 Reader 的 REPLICA_LAG_IN_MILLISECONDS,選擇延遲最低的節點連線 |
優點:毫秒級延遲監控精度;可實作智能路由選擇最佳節點;Aurora 原生支援
缺點:需要額外查詢監控表;需要應用層實作選擇邏輯;增加架構複雜度
AWS RDS Multi-AZ
| 項目 | 說明 |
|---|---|
| 場景 | 傳統製造業公司的內部 ERP 系統,使用量不大但資料極為重要,預算有限不想用 Aurora,但仍需要基本的災難恢復能力確保機房故障時系統能繼續運作 |
| 痛點 | 單一 AZ 發生故障(如機房停電、網路中斷)時,資料庫完全無法存取,業務停擺 |
| 策略 | 啟用 RDS Multi-AZ,AWS 自動在另一個 AZ 維護同步備援實例,故障時自動切換 |
優點:配置簡單一鍵啟用;同步複製確保資料不丟失;自動故障轉移;無需管理複製邏輯
缺點:備援實例不可用於讀取分流(純備援);成本約加倍;故障轉移時間較長(1-2 分鐘)
AWS RDS Multi-AZ + Read Replica
| 項目 | 說明 |
|---|---|
| 場景 | 媒體網站在 AWS 上運行,文章頁面讀取量極大(讀寫比 100:1),需要擴展讀取能力,同時也需要確保主庫故障時資料不丟失且能自動恢復服務 |
| 痛點 | Multi-AZ 的備援實例不能用於讀取分流;單一主庫無法承受大量讀取請求導致網站變慢 |
| 策略 | 同時啟用 Multi-AZ(高可用)和創建 Read Replica(讀取擴展),分別解決不同問題 |
優點:兼具高可用和讀取擴展;Read Replica 可跨 AZ 或跨 Region 部署;架構靈活
缺點:Read Replica 使用異步複製會有延遲;成本較高;需要應用層處理讀寫分離
快速決策表
依一致性需求選擇
| 場景特徵 | 推薦策略 | 一句話理由 |
|---|---|---|
| 報表、分析、歷史數據 | 直接走從庫(EVENTUAL) | 延遲可接受,不影響主庫 |
| 寫入後需要立即讀取 | 強制走主庫 | 最簡單直接,100% 一致 |
| 精確控制一致性(8.0.14+) | GR 一致性等級 BEFORE | MySQL 自動處理等待 |
| 寫入後需要全局可見 | GR 一致性等級 AFTER | 確保所有節點都能讀到 |
| 金融交易、零容忍不一致 | GR BEFORE_AND_AFTER | 最強一致性保證 |
| 舊版 MySQL 需要解決過期讀 | Semi-Sync + GTID/位點等待 | 傳統方案,廣泛適用 |
依環境選擇
| 環境 | 推薦策略 | 一句話理由 |
|---|---|---|
| 自建 MySQL 5.5+ | Semi-Sync | 簡單有效防止資料丟失 |
| 自建 MySQL 8.0.14+ | Group Replication | 強一致 + 自動故障轉移 |
| 自建 MySQL 8.2+ | MySQL Router + GR | 應用層零改動 |
| AWS 新專案 | Aurora | 全托管、低延遲、高可用 |
| AWS 預算有限 | RDS Multi-AZ | 基本高可用保障 |
| AWS 需要讀取擴展 | Aurora 或 RDS + Read Replica | 可水平擴展讀取能力 |
依 MySQL 版本選擇
| MySQL 版本 | 可用的一致性策略 | 可用的資料保護策略 |
|---|---|---|
| 5.5 ~ 5.6 | 強制走主庫、位點等待 | Semi-Sync |
| 5.7 ~ 8.0.13 | 強制走主庫、GTID 等待 | Semi-Sync、基礎 GR |
| 8.0.14 ~ 8.1 | GR 一致性等級(BEFORE/AFTER) | Group Replication |
| 8.2+ | GR 一致性等級 + MySQL Router 透明讀寫分離 | Group Replication + Router |
| AWS Aurora | 強制走主庫、replica_host_status 監控 | Aurora 儲存層自動處理 |
總結
解決「過期讀」問題的關鍵:根據場景選擇適當的一致性等級。
| 問題 | 緊急解 | 長期解 |
|---|---|---|
| 過期讀 | 強制走主庫、Sleep 等待 | GR 一致性等級、GTID 等待 |
| 資料丟失 | Semi-Sync | Group Replication |
| 故障轉移 | 手動切換 | GR + MySQL Router |




