結論先講
讀寫分離的 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 可能看到舊資料。
解法:
- 寫入後的讀取強制走 Primary(需要 ORM/middleware 支援)
- 設定 synchronous replication(Replica 確認收到才 commit,但會拖慢寫入)
- 接受延遲(某些場景 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 篇。
和系列其他文章的關聯
- 連線池調整(先做這個)→ 第 19 篇
- Redis cache(先做這個)→ 第 33 篇
- PG Replication 設定 → 第 31 篇
- 免費午餐的 ROI → 第 21 篇
- 水平擴展時的 DB 連線 → 第 22 篇
下一篇
最佳化路線圖:五層結論 + 該先做什麼 — 所有深入主題都講完了,接下來把所有優化方案按 ROI 排序。
本系列文章
完整 68 篇目錄見 系列首頁
← 上一篇:樂觀鎖 vs 悲觀鎖:實作細節與踩坑指南 → 下一篇:最佳化路線圖:五層結論 + 該先做什麼後做什麼