前言

現實商務環境中,MySQL 讀寫分離是非常常見的架構。但當你寫入資料後立即查詢,卻發現資料「消失」了?這就是經典的「⚠️ 過期讀」問題!本文將深入分析原因與解決方案。


讀寫分離文章重點快速理解一圖流

一圖流展開/收合

讀寫分離一圖流


情境沙雕動畫

情境影片,歡迎輕鬆觀閱!

為什麼採用「一寫多讀」架構?

主流商業環境都採用「一寫多讀」而非「多寫多讀」架構,原因是什麼?

架構說明風險
一寫多讀單一主庫負責寫入,多個從庫負責讀取無資料衝突風險,但可能發生過期讀
多寫多讀多個主庫都可寫入當 Binlog 延遲時,除了過期讀,更會發生資料間的 PK 衝突(如兩邊同時新增 ID=10)

結論:目前我自己實務經驗中,幾乎接觸到的都是一寫多讀居多,幾乎沒遇過多寫多讀的架構設計,所以這篇文章也主要 針對一寫多讀來進行詳細的技術討論。

(備註:未來如果有特殊案例使用多寫多讀的架構,我將在補充在這篇文章。)


讀寫分離的同步原理

在深入問題之前,先了解讀寫分離的同步流程(以下僅列重點流程,不會將完整的 InnoDB 下的 two-phase commit 都展開):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Step 1. 主庫異動資料(執行新增、修改或刪除操作)
Step 2. 將異動的 Query 寫入 Binlog
Step 3. 執行 Commit,主庫異動成功,主庫的 Dump Thread 傳送 Binlog 給從庫的 I/O Thread
Step 4. 從庫透過 I/O Thread 監聽 Binlog
Step 5. 從庫將 Binlog 中異動內容暫存到 Relay log
Step 6. 從庫 SQL Thread 監聽 Relay Log,一旦有異動則執行 commit
Step 7. 從庫資料與主庫資料同步成功

備註:正常情況下,同步時間約為毫秒(ms)等級。

(以 AWS Aurora 為例)
查詢指令:
SELECT * FROM information_schema.replica_host_status;

查詢結果範例:
| SERVER_ID | HOST | PORT | MASTER_SERVER_ID | REPLICA_LAG_IN_MILLISECONDS |
|-----------|------|------|------------------|----------------------------|
| 1001 | aurora-reader-1 | 3306 | 1000 | 15 |

⭐ REPLICA_LAG_IN_MILLISECONDS 欄位:顯示從庫與主庫的延遲時間(單位:毫秒),正常情況下應接近 0。

問題:什麼情況會導致「過期讀」?

「過期讀」指的是:從庫的資料尚未與主庫同步,導致查詢到舊資料。

三大常見原因

原因說明
Big Transaction一次刪除百萬筆資料,主庫 Binlog 快速寫入,但從庫需要較長時間同步
高併發主庫效能高、從庫較弱時,SQL Threads 需排隊處理
網路問題跨區部署(如美國↔日本)會有網路延遲

監控指標: 在 AWS CloudWatch 可監控 ReplicaLag

  • 數值 = 0:完全同步
  • 數值 = 10:從庫資料落後主庫 10 秒

解法一:緊急止血方案

需要快速上線、無法大改架構時的臨時解法。

強制走主庫

直接放棄讀寫分離,所有查詢都走主庫。

優點:最簡單、100% 保證資料一致性

缺點:失去讀寫分離的意義,主庫負擔加重

Sleep 等待

在查詢前加入 Sleep,等待從庫同步完成。

1
2
3
// 寫入後等待 1 秒再查詢
usleep(1000000); // 1 秒
$result = $slaveDb->query("SELECT * FROM users WHERE id = ?", [$userId]);

優點:實作簡單

缺點:不精確、浪費時間、非主流做法

缺點:僅適合緊急情況,長期不建議使用


延伸:解決 Sleep 不精確的最佳解

Sleep 的問題在於「不精確」——延遲 400ms 時你等了 2 秒是浪費,延遲 2.5 秒時你等 2 秒又不夠。以下是更精確的做法。

查詢同步狀態

1
2
3
4
5
-- MySQL 8.0.22+
SHOW REPLICA STATUS;

-- MySQL 8.0.21 及更早版本
SHOW SLAVE STATUS;

三種判斷方式

方式說明精確度
seconds_behind_master查看是否為 0(單位:秒)⭐ 低
對比位點比對 Master_Log_FileRelay_Master_Log_File⭐⭐ 中
對比 GTID比對 Retrieved_Gtid_SetExecuted_Gtid_Set⭐⭐⭐ 高

對比位點詳解

1
2
3
4
Master_Log_File 和 Read_Master_Log_Pos:讀到的主庫最新位點
Relay_Master_Log_File 和 Exec_Master_Log_Pos:從庫執行的最新位點

→ 兩組值完全相同,表示已同步完成

對比 GTID 詳解

1
2
3
4
5
Auto_Position=1:表示使用 GTID 協議
Retrieved_Gtid_Set:從庫收到的所有日誌 GTID 集合
Executed_Gtid_Set:從庫已執行完成的 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
2
3
4
5
6
7
8
-- 連線到從庫
SET @@SESSION.group_replication_consistency = 'BEFORE';

-- 這行 SELECT 會等待從庫同步完成後才執行
SELECT * FROM students WHERE id = 3;

-- 恢復預設
SET @@SESSION.group_replication_consistency = 'EVENTUAL';

主庫使用範例(AFTER)

1
2
3
4
5
6
7
8
-- 連線到主庫
SET @@SESSION.group_replication_consistency = 'AFTER';

-- 這行 UPDATE 會等待所有從庫套用完成才回傳成功
UPDATE students SET score = 80 WHERE id = 3;

-- 恢復預設
SET @@SESSION.group_replication_consistency = 'EVENTUAL';

解法三: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
2
3
4
5
6
7
8
9
10
-- 設定強制全局一致性
-- 'global' 意思是強制要求從庫必須追上主庫最新的事務位置才能回傳結果
SET @@SESSION.aurora_replica_read_consistency = 'global';

-- 這行 SELECT 會等待從庫追上主庫
-- 如果還沒追上,會 Block 住直到資料同步完成(最多等待 max_execution_time)
SELECT * FROM students WHERE id = 3;

-- 恢復預設
SET @@SESSION.aurora_replica_read_consistency = '';

優點:Aurora 原生支援,實作簡單

缺點:增加查詢延遲


進階議題:資料保護機制(斷電、特殊情況處理)

情境: 如果遭遇斷電或特殊狀況,導致主庫同步 Binlog 到從庫時出現意外中斷,資料遺失該怎麼辦?

重要: Semi-Sync 預設是關閉的,需要手動安裝 Plugin 並啟用!

Semi-Sync 半同步複製

確保 Binlog 傳到從庫後才回應 Client。

ACK 定義: ACK 是一種「確認訊號(acknowledgement)」,表示收到資料並成功處理過的回應。

執行流程:

1
2
3
4
主庫寫 Binlog 
→ 傳給從庫
→ 從庫寫入 Relay Log 後回傳 ACK
→ 主庫收到 ACK 後才回應 Client Commit

如何開啟 Semi-Sync:

以下指令皆在 MySQL Client 中執行(透過 mysql -u root -p 連線後執行)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-- ========== 主庫設定(連線到主庫執行)==========
-- 1. 安裝 Semi-Sync Plugin
INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so';

-- 2. 啟用 Semi-Sync
SET GLOBAL rpl_semi_sync_master_enabled = 1;

-- 3. 設定等待超時時間(預設 10 秒,可依需求調整)
SET GLOBAL rpl_semi_sync_master_timeout = 10000;

-- ========== 從庫設定(連線到從庫執行)==========
-- 1. 安裝 Semi-Sync Plugin
INSTALL PLUGIN rpl_semi_sync_slave SONAME 'semisync_slave.so';

-- 2. 啟用 Semi-Sync
SET GLOBAL rpl_semi_sync_slave_enabled = 1;

-- 3. 重啟 Slave IO Thread 使設定生效
STOP SLAVE IO_THREAD;
START SLAVE IO_THREAD;

檢查 Semi-Sync 狀態:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 查看 Plugin 是否已安裝
SHOW PLUGINS;

-- 查看 Semi-Sync 相關變數(主庫)
SHOW VARIABLES LIKE 'rpl_semi_sync_master%';

-- 查看 Semi-Sync 相關變數(從庫)
SHOW VARIABLES LIKE 'rpl_semi_sync_slave%';

-- 查看 Semi-Sync 運行狀態(主庫)
SHOW STATUS LIKE 'Rpl_semi_sync_master%';

-- 查看 Semi-Sync 運行狀態(從庫)
SHOW STATUS LIKE 'Rpl_semi_sync_slave%';

關鍵狀態指標:

變數說明
Rpl_semi_sync_master_statusON 表示主庫 Semi-Sync 運行中
Rpl_semi_sync_slave_statusON 表示從庫 Semi-Sync 運行中
Rpl_semi_sync_master_clients已連線的 Semi-Sync 從庫數量

永久生效: 上述設定重啟後會失效,若要永久生效需寫入 my.cnf

1
2
3
4
5
6
7
8
# 主庫
[mysqld]
rpl_semi_sync_master_enabled = 1
rpl_semi_sync_master_timeout = 10000

# 從庫
[mysqld]
rpl_semi_sync_slave_enabled = 1

優點:有效防止資料丟失

缺點:增加寫入延遲

AFTER_SYNC 無損複製

MySQL 8.0 開啟 Semi-Sync 時,預設使用 AFTER_SYNC 模式(wait_point=AFTER_SYNC)。

MySQL 8.0+ 開啟方式(指令有變更):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- ========== 主庫設定 ==========
-- 1. 安裝 Plugin
-- MySQL 8.0.26+ 已將 Semi-Sync 改為內建功能,不再需要安裝 Plugin
-- MySQL 8.0.25 及更早版本需要執行以下指令:
INSTALL PLUGIN rpl_semi_sync_source SONAME 'semisync_source.so';

-- 2. 啟用 Semi-Sync
SET GLOBAL rpl_semi_sync_source_enabled = 1;

-- ========== 從庫設定 ==========
INSTALL PLUGIN rpl_semi_sync_replica SONAME 'semisync_replica.so';

-- 2. 啟用 Semi-Sync
SET GLOBAL rpl_semi_sync_replica_enabled = 1;

-- 3. 重啟 Replica IO Thread
STOP REPLICA IO_THREAD;
START REPLICA IO_THREAD;

名稱變更: 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
2
3
4
1. 主庫寫入事件
2. Transaction 提交前送給所有成員(透過 GCS Paxos-like 協議)
3. 需要多數節點(quorum)同意才 Commit
4. 每個成員套用到本地 InnoDB

斷電場景:

  • 若所有節點未達成 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 一致性等級 BEFOREMySQL 自動處理等待
寫入後需要全局可見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.1GR 一致性等級(BEFORE/AFTER)Group Replication
8.2+GR 一致性等級 + MySQL Router 透明讀寫分離Group Replication + Router
AWS Aurora強制走主庫、replica_host_status 監控Aurora 儲存層自動處理

總結

解決「過期讀」問題的關鍵:根據場景選擇適當的一致性等級

問題緊急解長期解
過期讀強制走主庫、Sleep 等待GR 一致性等級、GTID 等待
資料丟失Semi-SyncGroup Replication
故障轉移手動切換GR + MySQL Router


延伸閱讀