Skip to main content

Command Palette

Search for a command to run...

Lesson 24: 資料庫擴展術-讀寫分離、複寫機制與快取一致性挑戰

Published
2 min read

為什麼要讀寫分離?

大多數的 Web 應用都是 「讀多寫少」(例如:看文的人多,發文的人少,Heavy Read System)。當所有的請求都塞給同一台資料庫時,磁碟 I/O 和連線數會成為瓶頸。

  • Master (主庫): 負責寫入 (Insert/Update/Delete),確保數據一致性。

  • Slave (從庫): 負責讀取 (Select),可以有多個從庫來分擔讀取壓力。

為什麼讀寫分離能提升效能?

  • 磁碟 I/O 分散: 寫入操作通常涉及索引更新和交易日誌,非常吃磁碟性能。將讀取抽離,可以避免 SELECT 語句與 INSERT/UPDATE 爭搶資源。

  • 併發連線數: 每台資料庫能承載的連線數有限,多台 Slave 代表能支撐更多的使用者同時在線瀏覽。

路由實作:由誰來決定 SQL 去哪裡

實務上,讀寫分離的「路由控制」有兩種主流架構,各有優缺點:

  • 應用層控制 (Application-side Routing):

    • 作法: 在程式碼或框架設定中定義 readwrite 連線(例如 Laravel 的 database.php)。

    • 優點: 效能最高(少一層網路跳轉)、部署簡單。

    • 缺點: 業務代碼與基礎設施耦合;當 Slave 增加或異動時,所有應用程式都得更新設定。

  • 代理層控制 (Proxy-side Routing):

    • 作法: 引入 ProxySQLMySQL Router。程式只連向 Proxy,由 Proxy 根據 SQL 語法(SELECT vs UPDATE)自動轉發。

    • 優點: 支援連線池 (Connection Pooling)、動態下線壞掉的 Slave、對程式碼透明。

    • 缺點: 增加系統複雜度與網路延遲 (Latency)。

資料庫複寫的原理

  • 非同步複寫 (Asynchronous): 主庫寫完立即回傳成功,再慢慢同步給從庫。效能最好,但有數據遺失風險(如果 Master 剛寫完就當機,資料可能還沒傳到 Slave,導致資料遺失)

  • 半同步複寫 (Semi-synchronous): 主庫寫完後,至少等一個從庫確認收到資料才回傳。平衡了效能與安全性。

  • Binary Log (Binlog): 這是 MySQL 同步的核心,從庫就是透過讀取主庫的 Binlog 來重演 (Replay) 資料操作。

複寫底層的細節:Binlog 格式的影響

Slave 是怎麼同步的?是同步 SQL 語句還是同步數據結果?這會影響同步的準確性與效能:

  • Statement-Based Replication (SBR): 同步 SQL 語句。

    • 坑: 如果 SQL 裡有 NOW()UUID(),Slave 執行的結果會跟 Master 不一樣。
  • Row-Based Replication (RBR): 同步實際變更的數據行。

    • 優點: 最安全,保證資料一致。

    • 缺點: 當大量更新時,Binlog 會噴發,佔用頻寬。

  • Mixed-Based Replication: 自動判斷。

在現代生產環境中,建議優先選用 Row-Based (RBR)。雖然它在大量更新(如 UPDATE users SET status = 1)時會產生巨大的 Log,但它能保證 100% 的資料精確度,避免 RAND()NOW() 造成的同步問題。

快取一致性挑戰

還記得我們在L21中快取的章節有充到:如果在主從分離的極端架構下,為了避免快取到尚未更新的從庫(舊資料),快取有兩個調整策略:延遲雙刪 & CDC。

這裡再提供一個方式 - 強制讀主庫: 如果快取過期時間很短,或者不希望快取邏輯太複雜,就會更依賴 「強制讀主庫」 來保證寫入後的第一次讀取是正確的。

強制讀主庫(Master-only Read)

核心邏輯:繞過讀寫分離的自動調度邏輯。

這通常發生在對「資料即時性」要求極高的場景,例如:使用者剛刷完卡、剛改完密碼,或者在交易(Transaction)程序中。

框架層級實作

這是最常見的做法,透過後端框架提供的 API,手動指定當前的 Query 使用「寫入連線」

$user = User::onWriteConnection()->find(userId);

在Laravel中還有一個特殊的選項:sticky

當sticky = true時,可用於允許立即讀取在目前請求週期內寫入資料庫的記錄。如果啟用了sticky選項,並且在目前請求週期內對資料庫執行了「寫入」操作,則任何後續的「讀取」操作都會使用「寫入」連線。

Laravel 官方文件沒告訴你的事情:在 Laravel 中,一旦進入 transaction,所有讀取會自動 fallback 到 write connection,因為 transaction state 僅存在於 write PDO。

ManagesTransactions.php 中,可以看到 function beginTransaction 呼叫了createTransaction

而createTransaction的code如下:

//ManagesTranstions
protected function createTransaction()
    {
        if ($this->transactions == 0) {
            $this->reconnectIfMissingConnection();

            try {
                $this->getPdo()->beginTransaction();
            } catch (Throwable $e) {
                \(this->handleBeginTransactionException(\)e);
            }
        } elseif (\(this->transactions >= 1 && \)this->queryGrammar->supportsSavepoints()) {
            $this->createSavepoint();
        }
    }

重點就在於getPdo,在Connection.php 中:

$this->pdo存放master connection

$this->readPdo存放slave connection

另外 在 getReadPdo() 也會發現 如果有transition會回傳master connection,因此在整個transaction的期間內,所有DB操作都會到master connection

if ($this->transactions > 0) {
	return $this->getPdo();
}

SQL Hint 層級 (Proxy/Driver Level)

如果是使用 ProxySQL、MySQL Router 或特定的資料庫中間件,它們通常支援在 SQL 語句中加入「註解(Comment)」作為 Hint,告訴代理層這條 SQL 不准分流。

/*+ MASTER */ SELECT * FROM users WHERE id = 123;

代理伺服器(如 ProxySQL)會解析這段註釋,即使它是 SELECT 開頭,也會被強制路由到配置好的 Writer Group。這對「程式碼與架構解耦」非常有幫助,開發者只需改 SQL 字串。

黏性主庫策略(Sticky Master / Session-based)

實作邏輯:

  1. 當使用者進行 寫入操作(如 POST/PUT/PATCH)時,後端在 Response 加入一個短期有效的 Cookie 或在 Redis 紀錄一個 Flag(例如 recently_updated:{user_id},有效期間 2 秒)。

  2. 在接下來的請求中,Middleware(中介層) 檢查該 Flag 是否存在。

  3. 如果存在,該 Request 內的所有讀取自動切換到 Master 連線;如果不存在,則回歸正常的讀寫分離模式。

從Rookie到Junior,一個後端成長的30堂課

Part 3 of 31

“從 Rookie 到 Junior:一個後端成長的 30 堂課” 是一套專為後端新手所設計的成長型技術系列文章。內容以實務為導向,逐步拆解後端工程的核心能力,包括程式語言基礎、架構思維、框架運作原理、業務邏輯設計、資料庫操作、以及常見的開發模式。 本系列的目標是協助讀者從零散的學習堆疊,建立成體系的後端知識框架。讀者能夠理解各語言背後不變的工程思維與設計原則。這套內容旨在讓學習者從「會寫程式」進入「能理解系統設計」的階段,逐步具備勝任 Junior Backend Engineer 的能力。

Up next

Lesson 23: 系統的緩衝區-Queue 佇列與非同步處理 (Asynchronous)

佇列 佇列的實作工具非常多,舉凡AWS SQS、RabbitMQ、Kafka…等。 佇列的特性,其實是一個非常強大的系統緩衝區,應用層面非常廣。 什麼是佇列? 佇列可以想像成,在既有流程中外,有另一個”水管”,來連接原有的資料流(或邏輯過程),其中 呼叫方將資料 推(Push)到水管中,接受方(監聽) 從水管中將資料拉(Pull)出處理 為什麼佇列是「強大的緩衝區」? 在同步處理中,系統像是一

More from this blog

Lesson 26 : 系統韌性的守護者-限流、熔斷與背壓的設計模式

當這幾個名詞出現後,代表我們進到了一個高併發/大流量的系統了。在這個章節中,我們一起來看看如何透過一些方式來避免高併發導致我們的系統crash掉。 限流(Rate Limiting) 相信大家對這個名詞並不陌生,限流其實就是字面上的含意,限制流量。 限流的目的是保護「接收方」,確保系統不會因為瞬間的高併發請求而癱瘓。 限流通常發生在 API Gateway 或服務的最前端。它像是一個夜店門口的保全

Mar 26, 20262 min read

Lesson 25: 淺談 單體架構、微服務架構與單/多租戶架構

過去我們討論了 要把程式寫在哪、程式要怎麼拆的題目,接下來我們來看看「如何服務不同客戶」,這些架構反映了軟體開發在擴充性與複雜度之間的權衡。 軟體架構深度解析:從系統拆分到商業規模化 在軟體工程的演進中,架構的選擇往往是在「開發效率」、「系統擴充性」與「營運成本」之間尋求平衡。我們可以從兩個核心維度來觀察這些架構:系統如何運行(單體 vs. 分散式) 以及 如何服務客戶(單租戶 vs. 多租戶)。

Mar 26, 20262 min read

面試經驗談 2025-2026

從2025年3月開始,我陸陸續續參與了從新創到上市櫃公司的Senior - Tech Lead的相關面試,其中有不乏 尊重面試者、展現高度專業的企業(公司),當然也有遇到幾場面試鬼故事,這篇文章主要分享我對於軟體工程師面試的方向分享,以及部分鬼故事,以此警惕自己不要成為這樣的面試官。 AI的洪流,改變了SWE的生態 LLM的發展確確實實的影響到了軟體工程師的生態。 過去受限於算力與資料規模,深度學

Mar 23, 20262 min read

Lesson 23: 系統的緩衝區-Queue 佇列與非同步處理 (Asynchronous)

佇列 佇列的實作工具非常多,舉凡AWS SQS、RabbitMQ、Kafka…等。 佇列的特性,其實是一個非常強大的系統緩衝區,應用層面非常廣。 什麼是佇列? 佇列可以想像成,在既有流程中外,有另一個”水管”,來連接原有的資料流(或邏輯過程),其中 呼叫方將資料 推(Push)到水管中,接受方(監聽) 從水管中將資料拉(Pull)出處理 為什麼佇列是「強大的緩衝區」? 在同步處理中,系統像是一

Mar 23, 20262 min read

Bennett's Tech Blog | 後端架構、系統設計

32 posts

來自台灣的軟體工程師,相信軟體可以改變世界