結論先講

讀寫分離的 ROI 沒有 Redis cache 高。 壓測數據:Redis cache-aside 在讀多場景提升 6.5 倍(第 21 篇),讀寫分離大約 +30~50%。而且讀寫分離多了 replica 的維護成本、replication lag、連線路由邏輯。先做 Redis cache + 調 connection pool,撐不住了再考慮讀寫分離。


讀寫分離是什麼

寫入 → Primary DB(唯一的寫入點)
讀取 → Replica DB(一個或多個唯讀副本)

Primary --replication--> Replica 1
                     --> Replica 2

所有寫入操作(INSERT/UPDATE/DELETE)走 Primary。讀取操作(SELECT)走 Replica。Primary 的變更透過 replication 同步到 Replica。

理論上的好處

  • 讀取不影響寫入:Replica 處理讀取,不搶 Primary 的資源
  • 水平擴展讀取:加 Replica 就能提升讀取能力
  • Primary 只做寫入:寫入效能更穩定

壓測數據怎麼說

Infra 層的壓測有測讀寫分離(PG Primary + Replica):

配置RPS提升
單一 PG(讀寫都打同一台)基準
PG Primary + 1 Replica+30~50%讀多場景

對比其他優化:

優化方式提升複雜度
Redis cache(讀多)+550%
Connection pool 調整+70%極低
Multi-worker+93%極低
讀寫分離+30~50%

Redis cache 的效果是讀寫分離的 10 倍以上,而且複雜度更低。


讀寫分離帶來的問題

問題 1:Replication Lag

Primary 寫入後,Replica 不會立刻看到——通常有 1-100ms 的延遲(PG streaming replication)。

用戶: 建立訂單 → 成功(寫入 Primary)
用戶: 立刻查看訂單列表 → 「找不到訂單」(讀取 Replica,還沒同步)

這是最常被低估的問題。 用戶操作後立刻查看,讀取走 Replica 可能看到舊資料。

解法:

  1. 寫入後的讀取強制走 Primary(需要 ORM/middleware 支援)
  2. 設定 synchronous replication(Replica 確認收到才 commit,但會拖慢寫入)
  3. 接受延遲(某些場景 100ms 延遲可以接受)

問題 2:連線路由

Application 要知道「這個查詢走 Primary 還是 Replica」:

# FastAPI:手動路由
@app.get("/orders")
async def list_orders():
    async with get_replica_session() as session:  # 讀取走 Replica
        return await session.execute(select(Order))
 
@app.post("/orders")
async def create_order(data: OrderCreate):
    async with get_primary_session() as session:  # 寫入走 Primary
        order = Order(**data.dict())
        session.add(order)
        await session.commit()

每個 endpoint 都要決定走哪個 DB。改錯了(寫入走 Replica)會直接報錯。

問題 3:ORM 設定複雜

# SQLAlchemy:雙引擎設定
primary_engine = create_async_engine("postgresql://primary:5432/db")
replica_engine = create_async_engine("postgresql://replica:5433/db")
 
# 還需要一個 routing strategy
class RoutingSession(Session):
    def get_bind(self, mapper=None, clause=None):
        if self._flushing:  # 寫入
            return primary_engine
        else:  # 讀取
            return replica_engine

大部分 ORM 不是原生支援讀寫分離,需要自己寫 routing logic 或用 middleware。

問題 4:多了一台機器要維護

  • Replica 的備份策略
  • Replica 掛了的 failover
  • Replication 斷了的偵測和修復
  • 監控 replication lag

什麼時候值得做

值得做

信號說明
Redis cache hit rate < 50%Cache 效果不好(資料變化頻繁),讀寫分離可能更適合
Primary CPU 被讀取佔滿寫入排隊等讀取完成
讀寫比 > 90:10絕大部分是讀取,加 Replica 效果好
需要報表查詢把報表查詢導到 Replica,不影響 Primary

不值得做

信號說明
Redis cache 還沒做先做 cache(+6.5 倍),效果更好
Connection pool 還沒調Pool=5 vs Pool=50 差 70%(第 19 篇
讀寫比 < 70:30寫入佔比高,加 Replica 效果有限
UV < 500單一 DB + cache 就夠了

決策順序

DB 效能不夠?
├── 1. 調 connection pool(0 成本,+70%)
├── 2. 加 Redis cache(低成本,+6.5 倍讀取)
├── 3. 優化查詢 + Index(低成本,3-5 倍)
├── 4. 讀寫分離(中成本,+30-50%)
└── 5. 分庫分表 / Sharding(高成本,最後手段)

讀寫分離是第 4 步,不是第 1 步。


如果決定做:PG 的設定

Streaming Replication(最簡單)

Primary postgresql.conf

wal_level = replica
max_wal_senders = 3

Replica recovery.conf(PG 12+ 用 standby.signal):

primary_conninfo = 'host=primary port=5432 user=replicator'

就這樣。PG 的 streaming replication 設定比 MySQL 簡單很多(第 31 篇)。

搭配 PgBouncer

Application → PgBouncer(路由)→ Primary(寫入)
                               → Replica(讀取)

PgBouncer 可以做連線池 + 讀寫路由,application 不需要管連線去哪裡。


讀寫分離 vs CQRS

維度讀寫分離CQRS
分離的是DB 連線(同一個 schema)整個 model(讀寫不同 schema)
複雜度
適合DB 效能優化業務邏輯複雜、讀寫 model 差異大
資料同步Replication(自動)Event-Driven(自己做)

讀寫分離是 infra 層面的優化,CQRS 是 application 層面的架構。 大部分應用只需要讀寫分離(如果需要的話),不需要 CQRS。

CQRS 的詳細討論見 第 47 篇


和系列其他文章的關聯



下一篇

最佳化路線圖:五層結論 + 該先做什麼 — 所有深入主題都講完了,接下來把所有優化方案按 ROI 排序。

本系列文章

完整 68 篇目錄見 系列首頁

← 上一篇:樂觀鎖 vs 悲觀鎖:實作細節與踩坑指南 → 下一篇:最佳化路線圖:五層結論 + 該先做什麼後做什麼