為什麼讀寫分離#
電商讀寫比通常 9:1 ~ 100:1。單庫扛不住讀流量時,讀寫分離是擴容的第一步:
Application
│
┌────┴────┐
▼ ▼
write to read from
主庫 從庫1, 從庫2, ...
│ ▲
└─binlog──┘主庫負責寫,從庫負責讀。讀流量水平擴展只要加從庫。
MySQL 主備複製細節(binlog 三種格式、雙 M、循環複製)已在 02-database/05 高可用架構 寫過。本章聚焦「實戰怎麼用、怎麼處理副作用」。
用戶端 vs 中介層#
兩種實作位置:
用戶端方案#
應用直接維護 master / replicas 連接池:
class DB:
def __init__(self):
self.master = create_pool(master_url)
self.replicas = [create_pool(url) for url in replica_urls]
def execute(self, sql):
if is_write(sql):
return self.master.execute(sql)
return random.choice(self.replicas).execute(sql)典型工具:Sharding-JDBC、TDDL。
優點:性能好(無 proxy 跳轉)。 缺點:每個應用都要配,加減節點要重啟。
中介層方案#
放一個 proxy:應用連 proxy,proxy 看 SQL 決定路由。
典型工具:MyCat、ProxySQL、Vitess。
優點:應用無感知,運維集中。 缺點:proxy 自己要 HA,增加一跳延遲。
選擇:小團隊用戶端方案;大型多語言團隊中介層。
副作用 1:複製延遲#
主寫了,從庫要幾十 ms 到幾秒才同步到。如果同個請求剛寫完馬上讀:
def buy(user_id, sku_id):
db.master.execute("INSERT INTO orders ...")
return db.replica.execute("SELECT * FROM orders WHERE user_id=?", user_id)
# ← 讀不到剛寫的 order!解 1:強制走主庫#
對「寫後立刻讀」的查詢,明確標記走 master:
db.master.execute("SELECT ...") # 顯式或框架支持「最近 N 秒內走主庫」hint。
解 2:判讀請求類型#
CRUD 動作中:
- 寫 → 主
- 寫後讀(同一 session)→ 主
- 一般讀 → 從
這個策略需要 session 範圍管理,框架支持。
解 3:等位點(GTID/binlog 位置)#
寫之後拿到 GTID,讀請求帶這個 GTID,從庫追上才回應:
SELECT WAIT_FOR_EXECUTED_GTID_SET('xxx', 1); -- 1 秒超時
SELECT ...精確但延遲高。
實務:90% 用方案 1 + 2 就夠。
副作用 2:Read-after-write 在 session 內#
def update_user_name(user_id, name):
db.master.execute("UPDATE users SET name=%s WHERE id=%s", name, user_id)
return db.master.execute("SELECT * FROM users WHERE id=%s", user_id) # 必走主判斷標準:「這個 session 剛剛寫過 → 之後的讀走主」。可以用 thread-local 變數紀錄:
class SessionRouting:
last_write_at = None
def execute(sql):
if is_write(sql):
self.last_write_at = now()
return master.exec(sql)
if self.last_write_at and now() - self.last_write_at < 5:
return master.exec(sql)
return replica.exec(sql)5 秒內走主、之後走從。
副作用 3:從庫掛了#
一個從庫掛了 → 應用不能繼續往那個 IP 送流量。需要:
- 健康檢查(每 N 秒 ping)
- 自動踢除壞節點
- 重新加入時 catch up 完成才接流量
ProxySQL、HAProxy 都做這件事。用戶端方案要自己實作或用 service mesh。
複製狀態機(Replicated State Machine)#
主備複製的理論基礎:「對相同的初始狀態,按相同順序執行相同操作 → 狀態一致」。
master:
state_0 ─op_1→ state_1 ─op_2→ state_2 ─op_3→ ...
replica:
state_0 ─op_1→ state_1 ─op_2→ state_2 ─op_3→ ... ← 同樣的 op 序列關鍵:
- deterministic operations(確定性操作)── 同樣輸入產同樣輸出
- 同序執行:op_1 必須在 op_2 之前
這就是為什麼 MySQL row-based binlog 比 statement-based 安全 ── statement 對 NOW()、RAND() 等非確定函式會在不同實例產生不同結果。
複製狀態機是所有分散式儲存的基礎模型:
| 系統 | 對應概念 |
|---|---|
| MySQL 主備 | binlog 序列 |
| Redis | repl-backlog(傳播 commands) |
| Raft / Paxos | log entry 序列 |
| Kafka | partition 內 message 序列 |
| etcd | Raft log |
| Spanner | Paxos log per shard |
理解了 RSM 一個,理解了所有的分散式日誌複製。
半同步複製#
純非同步複製:master 寫完 binlog 立刻回應,replica 是否同步是後話 → master 突然掛了,binlog 還沒傳出去 → 資料丟。
半同步:master 等至少一個 replica 確認收到 binlog 才回應。
client → master: write
master → replica: send binlog
master ← replica: ack received
master → client: success代價:寫延遲 + 網路 RTT。但提供「至少一個從庫有資料」保證。
GitHub 的 OrcheStrator + 半同步 + 自動切換是經典設定。
從庫的延遲監控#
關鍵指標:Seconds_Behind_Master:
SHOW SLAVE STATUS\G
-- Seconds_Behind_Master: 0但這個值不準:它只計算「執行 SQL_thread 落後多少」,不計入 IO_thread 還沒收到的部分。
更可靠:用 heartbeat 表:
-- 主庫每秒寫
UPDATE heartbeat SET ts = NOW() WHERE id = 1;
-- 從庫測
SELECT NOW() - ts FROM heartbeat WHERE id = 1;差距即為實際延遲。
多從庫的負載分配#
簡單做法:random 或 round-robin。
進階:
- 權重:給性能好的機器多分流量
- 延遲感知:延遲高的少給流量
- 地域感知:ap-east 應用優先 ap-east 從庫
- 業務分層:報表類查詢走專屬 reporting replica,避免影響線上查詢
從庫的特殊用途#
從庫不只是讀流量分散,還可:
- 報表 / OLAP:跑大查詢不影響主庫
- 備份來源:對從庫做 dump,不影響主庫 IO
- 延遲從庫:故意設定 1 小時延遲,誤刪可從這邊救
- 災難備援:跨機房從庫
- schema 變更測試:先改從庫看效果
故障切換(failover)#
主庫掛了怎麼辦?
手動切換#
DBA 操作:選一個從庫 promote 成新主、其他從庫指向新主、修改應用設定。
自動切換(HA)#
工具:Orchestrator、MHA、群集化方案(MySQL Group Replication、Galera)。
關鍵問題:
- 如何判定主掛了:純 ping 不夠,因為網路抖動 ≠ 真死。需要 quorum 判斷
- 避免 split brain:兩個主同時自認 master → 雙寫災難。靠 fencing 或 majority quorum
- 資料丟失最小化:選 binlog 最新的從庫 promote
- 應用感知:透過 VIP / DNS / proxy 切換,應用無痛
主流方案:MySQL 8.0 InnoDB Cluster(原生)、Vitess(YouTube/Slack 用)。
主主複製(雙寫)的陷阱#
「兩個都當主,互相同步」── 看似簡單,但:
- 自增主鍵衝突:兩邊 INSERT 同一個 ID
- 解:兩邊用不同 step / offset
- 同行併發更新衝突:兩邊都改同一筆 → 不知誰贏
- 複製延遲下的不一致:A 寫了還沒到 B、B 也寫了
- 唯一索引衝突:兩邊各 INSERT 同一個 unique value
實務上不建議業務雙寫。雙 M 結構通常是「同時只有一邊接寫流量」── 另一個是熱備。
群集化複製:Galera / Group Replication#
避免雙寫陷阱的 ── 同步多寫:
- 寫之前先在所有節點達成共識
- 衝突在提交前偵測
Galera Cluster:第三方,xtrabackup 出身。 MySQL Group Replication(GR):8.0+ 原生。
特性:
- 強一致性
- 任一節點都可寫
- 多數派提交(容錯一個節點掛)
代價:
- 寫延遲 = 跨網路共識
- 大事務性能差
- 複雜度高
適合:多機房強一致需求。不適合:寫密集、低延遲需求。
小結#
- 讀寫分離:擴容讀流量的標準動作
- 副作用:複製延遲 → 寫後立讀走主、session 內走主、必要時等位點
- 複製狀態機是所有分散式日誌複製的理論基礎
- 半同步:用一點延遲換資料安全
- 故障切換:手動可控、自動快但要 fencing
- 業務雙寫一般不建議;強一致用 Group Replication / Galera
下章看「進一步擴容」── 分庫分表,當單庫單表不夠的時候。