返回首頁高併發

高併發資料庫設計:讀寫分離、分庫分表與 Redis 快取策略|2025

20 min 分鐘閱讀
#高併發資料庫#Redis#讀寫分離#分庫分表#快取穿透#快取擊穿#快取雪崩#MySQL#雲端資料庫

高併發資料庫設計:讀寫分離、分庫分表與快取策略

前言:資料庫是高併發系統最常見的瓶頸

系統變慢了。你打開監控一看,CPU 還好、記憶體還好、網路還好。

問題出在資料庫。

查詢堆積、連線數飆高、回應時間從 50ms 變成 5 秒。這是高併發系統最常見的場景。

根據經驗,90% 的系統效能問題出在資料層

本文將帶你從瓶頸分析開始,一路講到讀寫分離、分庫分表、Redis 快取設計,以及那三個讓人頭痛的快取問題:穿透、擊穿、雪崩。

如果你還不熟悉高併發的整體架構,建議先閱讀高併發是什麼?完整指南


一、資料庫瓶頸分析

在優化之前,你需要知道問題出在哪裡。

1.1 連線數限制

每個資料庫連線都要佔用記憶體。MySQL 預設最大連線數是 151,PostgreSQL 是 100。

當併發請求超過連線數上限,新的請求只能等待。等待太久就超時,用戶看到錯誤。

症狀

  • Too many connections 錯誤
  • 應用程式拿不到連線
  • 請求排隊等待

解法

  • 使用連線池(HikariCP、PgBouncer)
  • 調高 max_connections(但有上限)
  • 讀寫分離,分散連線

1.2 讀寫競爭

資料庫有鎖機制。寫入時可能鎖住整張表或整行,其他請求只能等。

高併發場景下,熱點資料被頻繁讀寫,鎖競爭變得嚴重。

症狀

  • 大量請求處於 waiting for lock 狀態
  • 寫入操作卡住讀取
  • 資料庫 CPU 使用率飆高但吞吐量不高

解法

  • 讀寫分離(讀不影響寫)
  • 樂觀鎖取代悲觀鎖
  • 減少鎖持有時間
  • 熱點資料放快取

1.3 單機容量上限

一台資料庫能存多少資料?能處理多少 QPS?都有上限。

當資料量達到幾億、幾十億筆,單機查詢效能會顯著下降。

症狀

  • 查詢時間隨資料量增加
  • 磁碟空間不足
  • 備份還原時間過長

解法

  • 分庫分表
  • 歸檔舊資料
  • 使用分散式資料庫

二、讀寫分離實作

讀寫分離是高併發資料庫優化的第一步。

2.1 原理說明

大多數應用是「讀多寫少」。電商網站 90% 的請求是瀏覽商品,只有 10% 是下單購買。

讀寫分離的思路:

  • 主庫(Master):負責所有寫入操作
  • 從庫(Slave):負責讀取操作,可以有多個
寫入請求 → 主庫
讀取請求 → 從庫 1 / 從庫 2 / 從庫 3

主庫的資料透過複製(Replication)同步到從庫。

2.2 實作方式

應用層實作

在程式碼中判斷 SQL 類型,決定要送到主庫還是從庫。

# 虛擬碼
if sql.startswith("SELECT"):
    connection = slave_pool.get_connection()
else:
    connection = master_pool.get_connection()

中間件實作

使用專門的中間件自動路由:

  • MySQL Proxy
  • ProxySQL
  • MaxScale
  • ShardingSphere

中間件的好處是對應用透明,不需要改 code。

雲端託管方案

主流雲端資料庫都支援讀寫分離:

  • AWS Aurora:自動讀寫分離 endpoint
  • GCP Cloud SQL:建立 Read Replica
  • Azure SQL:設定 Read Scale-Out

2.3 同步延遲問題

從庫的資料不是即時同步的,有延遲(通常幾毫秒到幾秒)。

這會造成「剛寫入的資料讀不到」的問題。

解決方案

  • 強制走主庫:對時效性要求高的查詢,強制從主庫讀取
  • 延遲感知路由:監控從庫延遲,延遲過高時切回主庫
  • 因果一致性:帶著寫入時的 GTID 去讀,確保從庫已同步

插圖 1:讀寫分離架構示意圖

三、分庫分表策略

當單機資料庫到達極限,分庫分表是下一步。

3.1 何時該分庫分表

該分的訊號

  • 單表資料量超過 1000 萬筆(經驗值)
  • 單庫大小超過 500GB
  • 單庫連線數不夠用
  • 查詢效能持續下降

不該分的情況

  • 資料量還小,優化 SQL 和索引就夠了
  • 沒有做好讀寫分離和快取
  • 團隊沒有分散式資料庫經驗

分庫分表會大幅增加複雜度,不是輕易的決定。

3.2 分庫策略

垂直分庫:按業務拆分

把不同業務的表放到不同資料庫:

  • 用戶庫:users、user_profiles
  • 訂單庫:orders、order_items
  • 商品庫:products、categories

好處是業務隔離,一個庫掛了不影響其他庫。

水平分庫:按規則拆分

把同一張表的資料分到多個資料庫:

  • 用戶庫 1:user_id 1-1000 萬
  • 用戶庫 2:user_id 1000 萬-2000 萬

好處是突破單機容量限制。

3.3 分表策略

垂直分表:拆列

把一張寬表拆成多張表:

  • users:id, name, email(常用欄位)
  • user_details:id, bio, avatar, preferences(不常用欄位)

減少單行大小,提升查詢效能。

水平分表:拆行

把資料按規則分到多張表:

  • orders_202401:2024 年 1 月的訂單
  • orders_202402:2024 年 2 月的訂單

或按 ID 取模:

  • orders_0:order_id % 4 = 0
  • orders_1:order_id % 4 = 1
  • orders_2:order_id % 4 = 2
  • orders_3:order_id % 4 = 3

3.4 分片鍵選擇

分片鍵(Sharding Key)決定資料分到哪個庫/表。選擇至關重要。

好的分片鍵特性

  • 查詢常用:大多數查詢都會帶這個欄位
  • 分布均勻:資料能均勻分散,不要傾斜
  • 不常變動:分片鍵改變會導致資料遷移

常見分片鍵

  • 用戶 ID(user_id)
  • 訂單 ID(order_id)
  • 時間(created_at)
  • 租戶 ID(tenant_id,SaaS 場景)

3.5 跨庫查詢難題

分庫分表後,最大的挑戰是跨庫查詢。

問題 1:跨庫 JOIN

用戶表在用戶庫,訂單表在訂單庫。要查「用戶的所有訂單」怎麼辦?

解法:

  • 應用層組裝(先查用戶,再查訂單)
  • 資料冗餘(訂單表存用戶名稱)
  • 寬表(把關聯資料提前合併)

問題 2:跨庫分頁

訂單分在 4 個庫,要取「最新 10 筆訂單」怎麼辦?

解法:

  • 每個庫取 10 筆,應用層合併排序
  • 搜尋引擎(Elasticsearch)做查詢
  • 避免深分頁

問題 3:全局唯一 ID

分庫後不能用自增 ID,因為每個庫都會產生重複的 ID。

解法:

  • UUID(簡單但佔空間)
  • Snowflake ID(Twitter 方案)
  • 資料庫號段(美團 Leaf)

四、Redis 快取設計

Redis 是高併發系統的標配。用對了,資料庫壓力減少 90%。

4.1 快取架構

請求 → 應用層 → Redis 快取 → 資料庫
                    ↓
              快取命中直接返回

Cache Aside Pattern(最常用)

  1. 先查快取
  2. 快取沒有,查資料庫
  3. 查到後寫入快取
  4. 返回結果

更新時:

  1. 更新資料庫
  2. 刪除快取(不是更新)

為什麼是「刪除」而不是「更新」?因為併發更新可能導致快取和資料庫不一致。刪除後,下次請求會重新從資料庫讀取。

4.2 快取策略

快取什麼?

  • 熱點資料(頻繁讀取)
  • 計算成本高的結果
  • 不常變動的資料

快取多久?

  • TTL(Time To Live)設定過期時間
  • 依據資料特性設定:熱點商品 5 分鐘、用戶資料 1 小時、靜態配置 1 天
  • 加入隨機值,避免同時過期

快取多大?

  • 評估熱點資料大小
  • 預留 20% 空間給突發流量
  • 設定淘汰策略(LRU 最常用)

4.3 分散式鎖

高併發場景需要分散式鎖來保護共享資源。

場景:秒殺扣庫存。100 個請求同時扣,如果不加鎖,可能超賣。

Redis 分散式鎖實作

# 加鎖
SET lock_key unique_value NX PX 30000

# 解鎖(用 Lua 腳本保證原子性)
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

注意事項

  • 設定過期時間,避免死鎖
  • 用唯一值識別鎖的擁有者
  • 解鎖時驗證是否是自己的鎖

如果需要更可靠的分散式鎖,考慮 Redlock 演算法或 Redisson。

更多秒殺設計細節,請參考高併發交易系統設計

4.4 效能調優

連線池配置

不要每次請求都新建連線。使用連線池,預建立連線重複使用。

Pipeline 批量操作

多個命令一起發送,減少網路往返。

pipe = redis.pipeline()
pipe.get("key1")
pipe.get("key2")
pipe.get("key3")
results = pipe.execute()

選擇正確的資料結構

  • 簡單 Key-Value:String
  • 物件屬性:Hash
  • 排行榜:Sorted Set
  • 佇列:List
  • 去重計數:Set / HyperLogLog

五、快取問題解決方案

用快取就會遇到這三個問題。面試必考,工作必踩。

5.1 快取穿透

問題描述

查詢一個不存在的資料。快取沒有,資料庫也沒有。每次請求都打到資料庫。

惡意攻擊者可以用這招打爆你的資料庫。

解決方案

方案 1:空值快取

資料庫查不到時,快取一個空值(TTL 設短一點)。

GET user:999999 → null(快取)
不用查資料庫

方案 2:布隆過濾器

在快取前加一層布隆過濾器。不存在的 Key 直接攔截,不查資料庫。

請求 → 布隆過濾器 → 快取 → 資料庫
           ↓
       不存在直接返回

布隆過濾器有一定的誤判率,但不會漏判。

5.2 快取擊穿

問題描述

某個熱點 Key 過期的瞬間,大量請求同時打到資料庫。

想像雙 11 的爆款商品,快取過期的那一刻,幾萬個請求同時去查資料庫。

解決方案

方案 1:互斥鎖

只有一個請求去查資料庫並更新快取,其他請求等待。

value = redis.get(key)
if value is None:
    if redis.setnx(lock_key, 1):  # 獲取鎖
        value = db.query(key)
        redis.set(key, value)
        redis.delete(lock_key)
    else:
        sleep(0.1)
        return get_with_lock(key)  # 重試
return value

方案 2:永不過期 + 後台更新

熱點 Key 不設過期時間,由後台定時更新。

快取:永不過期
後台任務:每 5 分鐘更新一次

5.3 快取雪崩

問題描述

大量 Key 同時過期,請求全部打到資料庫。或者 Redis 本身掛掉。

解決方案

方案 1:過期時間加隨機值

ttl = base_ttl + random.randint(0, 300)  # 基礎 TTL + 0-300 秒隨機
redis.setex(key, ttl, value)

方案 2:多層快取

本地快取 + Redis + 資料庫。Redis 掛了還有本地快取頂著。

方案 3:Redis 高可用

使用 Redis Sentinel 或 Redis Cluster,避免單點故障。

方案 4:熔斷降級

當資料庫壓力過大,觸發熔斷,返回預設值或錯誤提示。

插圖 2:快取三大問題對比圖

資料庫撐不住流量? 從讀寫分離到分庫分表,每一步都有陷阱。 預約架構諮詢,讓有經驗的顧問幫你規劃資料層優化方案。


六、雲端資料庫選型

如果你用雲端,不需要自己管理資料庫。但要選對服務。

6.1 AWS 方案

服務類型適用場景
RDS關聯式傳統應用,MySQL/PostgreSQL
Aurora關聯式(雲原生)高併發 MySQL/PostgreSQL,自動擴展
DynamoDBNoSQL(Key-Value)超高吞吐,無伺服器
ElastiCache快取Redis/Memcached 託管

Aurora 亮點

  • 比 RDS MySQL 快 5 倍
  • 自動擴展到 128TB
  • 15 個讀取副本
  • 跨可用區高可用

6.2 GCP 方案

服務類型適用場景
Cloud SQL關聯式MySQL/PostgreSQL/SQL Server
Cloud Spanner關聯式(分散式)全球一致性,超大規模
FirestoreNoSQL(文件)行動應用、即時同步
Memorystore快取Redis 託管

Cloud Spanner 亮點

  • 水平擴展的關聯式資料庫
  • 全球分散式
  • 強一致性(這很難得)
  • 99.999% SLA

6.3 Azure 方案

服務類型適用場景
Azure SQL關聯式SQL Server 生態
Cosmos DBNoSQL(多模型)全球分散、多種 API
Azure Cache快取Redis 託管

Cosmos DB 亮點

  • 多模型(文件、Key-Value、圖形、列族)
  • 全球分散式
  • 多種一致性級別可選
  • 毫秒級回應

6.4 選型比較表

需求AWSGCPAzure
MySQL 託管AuroraCloud SQLAzure SQL
超大規模關聯式AuroraSpannerAzure SQL Hyperscale
全球分散式 NoSQLDynamoDB GlobalSpanner / FirestoreCosmos DB
Redis 託管ElastiCacheMemorystoreAzure Cache

更詳細的雲端架構比較,請參考雲端高併發架構


七、實戰案例

案例:電商訂單系統優化

背景

  • 日訂單量 50 萬筆
  • 大促期間 QPS 突破 10,000
  • 資料庫經常撐不住

優化步驟

第一步:加快取

  • 商品資訊放 Redis,TTL 5 分鐘
  • 資料庫讀取減少 70%

第二步:讀寫分離

  • 主庫寫入,兩個從庫讀取
  • 使用 ProxySQL 做路由
  • 從庫分擔 80% 讀取流量

第三步:訂單表分表

  • 按月分表:orders_202501、orders_202502
  • 歷史訂單歸檔到冷儲存
  • 熱資料查詢效能提升 3 倍

結果

  • QPS 從 3,000 提升到 15,000
  • 資料庫 CPU 使用率從 90% 降到 40%
  • 大促期間系統穩定

常見問題 FAQ

Q1: 什麼時候該用 Redis?

只要是「讀多寫少」的熱點資料,就適合放 Redis。包括:Session、商品資訊、排行榜、計數器、分散式鎖。

Q2: 讀寫分離會影響資料一致性嗎?

會有延遲,通常幾毫秒到幾秒。對於「剛寫入就要讀」的場景,需要強制從主庫讀取。

Q3: 分庫分表後怎麼做報表?

分庫分表後,跨庫查詢很麻煩。報表建議用獨立的資料倉儲(如 ClickHouse、BigQuery),定時從業務庫同步資料。

Q4: NoSQL 可以取代關聯式資料庫嗎?

看場景。需要複雜查詢、JOIN、交易的場景,關聯式資料庫依然是首選。NoSQL 適合高吞吐、Schema 靈活的場景。

Q5: 快取和資料庫不一致怎麼辦?

使用「刪除快取」而非「更新快取」策略。如果還是不一致,設定較短的 TTL,或使用 CDC(Change Data Capture)同步。


結論:資料層優化是系統效能的關鍵

資料庫是高併發系統的生命線。優化對了,效能提升 10 倍不是夢。

本文重點回顧

  1. 資料庫瓶頸通常出在連線數、鎖競爭、單機容量
  2. 讀寫分離是第一步,用從庫分擔讀取壓力
  3. 分庫分表突破單機限制,但複雜度大增
  4. Redis 快取減少 90% 資料庫壓力
  5. 快取穿透用空值或布隆過濾器解決
  6. 快取擊穿用互斥鎖或永不過期解決
  7. 快取雪崩用隨機 TTL 和高可用解決
  8. 雲端資料庫省心,但要選對服務

延伸閱讀:


架構設計需要第二意見?

好的資料層設計能節省數倍的成本。如果你正在:

  • 資料庫撐不住流量,需要優化
  • 評估是否該分庫分表
  • 選擇雲端資料庫服務

預約架構諮詢,讓我們一起檢視你的資料層架構。

所有諮詢內容完全保密,沒有銷售壓力。


參考資料

  1. Martin Kleppmann,《Designing Data-Intensive Applications》(2017)
  2. Redis 官方文檔,《Redis Best Practices》(2024)
  3. AWS,《Amazon Aurora User Guide》(2024)
  4. Google Cloud,《Cloud Spanner Best Practices》(2024)
  5. 阿里巴巴,《阿里雲資料庫最佳實踐》(2023)

需要專業的雲端建議?

無論您正在評估雲平台、優化現有架構,或尋找節費方案,我們都能提供協助

預約免費諮詢

相關文章