## 資料庫交易 (Database Transaction) 與 ACID
資料庫交易 (Transaction) 是指將「一連串的操作」視為「一個不可分割的整體」。這個整體必須遵守 "All or Nothing" 原則:要嘛全部成功,要嘛全部失敗(就像沒發生過一樣),絕不允許只做一半。
為了保證這一點,傳統的關聯式資料庫 (如 MySQL, PostgreSQL) 遵循 ACID 屬性。
* ATM 轉帳
* 想像你要用 ATM 轉帳 1000 元 給你的朋友。這個動作在電腦裡其實分兩步:
* 步驟 A:從你的帳戶扣掉 1000 元。
* 步驟 B:在你朋友的帳戶加上 1000 元。
* 情境: 如果 步驟 A 做完的瞬間,銀行突然停電了,步驟 B 還沒做。
* 沒有交易機制:你的錢少了,朋友也沒收到。錢憑空消失了!(這會引發暴動)。
* 有交易機制 (ACID):銀行系統發現步驟 B 失敗了,它會自動執行 「回滾 (Rollback)」,把步驟 A 撤銷,把 1000 元還給你。對你來說,就像轉帳從未發生過。
在軟體工程中,我們通常使用 SQL (Relational Database) 來處理這種需要高度準確的資料。
1. Begin Transaction:告訴資料庫「我要開始做一連串動作了,請幫我盯著」。
2. 執行操作:修改資料 (扣錢、加錢、扣票)。
3. 鎖定 (Locking):在交易過程中,為了怕別人插手,資料庫通常會暫時「鎖住」這筆資料。例如在你轉帳時,別人不能同時修改你的餘額。
4. Commit (提交) 或 Rollback (回滾):
* 如果所有步驟都順利,執行 COMMIT,資料正式寫入,永久生效。
* 如果有任何錯誤,執行 ROLLBACK,恢復原狀。
這就是 ACID 中的 A (Atomicity,原子性):不可分割。

### 權衡 (Trade-offs)
既然 Transaction 這麼安全,為什麼我們不乾脆所有事情都用它?
* 優點 (Pros):
* 強一致性 (Strong Consistency):資料絕對準確,不會發生「錢不見」或「一張票賣給兩個人」的情況。
* 安全性:開發者不需要自己寫程式去檢查資料有沒有爛掉,資料庫幫你把關。
* 缺點/代價 (Cons):
* 效能瓶頸 (Performance):為了保證安全,資料庫會用到「鎖 (Lock)」。就像只有一個櫃台的銀行,當你在辦手續時,下一個人必須排隊等待。如果幾百萬人同時搶票,所有人都卡在資料庫排隊,系統會變得非常慢,甚至崩潰。
* 擴展困難:ACID 通常只能在「單一台」資料庫機器上運作得最好。當你需要把資料散佈到多台機器時,要維持 ACID 非常非常困難。
### 問題
假設我們正在賣 五月天演唱會 的票,只剩 1 張 票。 此時,小明 和 小華 兩個人,在完全相同的時間點 (毫秒級別的同步) 按下了「購買」按鈕。
利用剛學到的 Transaction (交易) 和 Lock (鎖) 的概念,你會怎麼描述資料庫裡發生了什麼事?為什麼最後只有一個人能買到,而不會兩個人都買到?
----
## 悲觀鎖 (Pessimistic Locking)
悲觀鎖 是一種「先佔先贏」的保護機制。它的預設心態很「悲觀」,認為「一定會有人來跟我搶這筆資料」,所以在我要修改資料之前,先把資料鎖住,直到我改完並提交 (Commit) 後才釋放,期間其他人想碰都不行。
在資料庫 (如 PostgreSQL/MySQL) 中,當你的程式執行到搶票邏輯時,會發生以下流程:
1. User A 的請求到達:資料庫執行 SELECT * FROM tickets WHERE id=1 FOR UPDATE;。
* 關鍵字是 FOR UPDATE。這告訴資料庫:「我要鎖住這一行 (Row),別人只能讀,不能改,也不能鎖。」
3. User B 的請求到達:也執行 SELECT ... FOR UPDATE。
* 資料庫會說:「抱歉,這行被 User A 鎖住了。」
* User B 的程式會進入 Block (阻塞/等待) 狀態,暫停執行。
5. User A 買票:將數量改為 0,執行 COMMIT。
6. 鎖釋放:User A 的交易結束,鎖自動解開。
7. User B 恢復執行:終於拿到了鎖,檢查票數... 發現是 0。
8. User B 失敗:回傳「票已售完」。
使用「悲觀鎖」雖然保證了絕對的安全,但代價是什麼?
* 資料絕對安全:完全避免「超賣」問題 (Double Booking)。
* 邏輯簡單:依賴資料庫原生功能,程式碼不用寫太複雜。
* 效能極差 (Performance Hit):這就是所謂的 「序列化 (Serialization)」。原本可以同時服務 1000 人的系統,現在變成要一個一個排隊上廁所。如果搶票時有 100 萬人,後面的人會等到天荒地老(Request Timeout)。
* 死鎖風險 (Deadlock):如果設計不好,可能會發生 A 等 B,B 等 A,兩人都卡死的狀況。

----
如果是 Ticketmaster 演唱會搶票,瞬間有 100 萬人 湧入,這 100 萬人都試圖擠進那個窄門(資料庫悲觀鎖)。
1. 資料庫會發生什麼事? (想想看那條排隊隊伍會有多長?)
2. 使用者的體驗會如何? (如果你是排在第 99 萬個的人,你的瀏覽器會顯示什麼?)
簡單回答就是會發生
* 請求超時 (Timeout)
* 連線池耗盡
----
## 樂觀鎖 (Optimistic Locking)
既然「鎖門」會讓大家堵死,那我們換個思維:「乾脆不鎖門了!」
樂觀鎖 的心態很「樂觀」。它假設「衝突不會經常發生」,所以允許多人同時讀取資料、同時計算。 但在最後寫入的那一瞬間,會檢查:「從我讀這筆資料到現在,有沒有別人偷改過?」
* 如果沒人改過 -> 寫入成功。
* 如果有人改過 -> 寫入失敗,請重試。
有用過 git 就會曉得大家都可以同時編輯,但是當今天要 push 上去,發現已經有新版本了,然後更慘的是文件有衝突,也就是說有人比你先一步更改了目前的文件,那就不能夠存你的版本,因為會覆蓋掉別人的,所以必須要先合併!
這就是類似樂觀鎖的概念,可以同時編輯,但是在最後寫入的時候,再去做確認!
### 技術細節
我們在資料庫表中增加一個欄位:version (版本號)。
搶票流程變成了這樣:
* User A 與 User B 同時讀取:
* 都讀到:Ticket ID=1, Stock=1, Version=1
* User A 動作快一點:
* 發送更新指令:UPDATE tickets SET stock=0, version=2 WHERE id=1 AND version=1
* User B 慢了一點:
* 發送更新指令:UPDATE tickets SET stock=0, version=2 WHERE id=1 AND version=1
* 資料庫檢查:目前版本是 1 嗎?不是! (因為剛剛被 A 改成 2 了)
* 更新失敗! (Affected Rows = 0)
注意: 這裡完全沒有「鎖 (Lock)」的動作,大家都可以隨便讀,只有在寫入那一刻才比對版本。
### 權衡 (Trade-offs)
樂觀鎖看起來很棒,解決了「排隊死鎖」的問題,但它完美嗎?
* 吞吐量高 (High Throughput):因為不鎖資料庫,大家都能讀,不會卡死連線。
* 適合「讀多寫少」:如果衝突機率低,效率極高。
* 高併發下的「重試風暴 (Retry Storm)」:
* 想像 1000 人搶 1 張票。
* 第 1 個人成功了。
* 剩下 999 個人在寫入時全部失敗。
* 這 999 個人通常會由程式自動發起「重試 (Retry)」。
* 這會導致 CPU 浪費在大量的失敗和重試上。
----
情境 A:賣冷門展覽的票(一天可能只有 10 個人買)。
情境 B:賣五月天演唱會的票(一秒鐘有 10 萬人買)。
針對 情境 B (五月天),如果你只能從「悲觀鎖」和「樂觀鎖」二選一,你會選哪一個?為什麼?或者你會覺得兩個都爛透了?
----
## 快取層原子計數 (Redis Atomic Counters)
既然資料庫 (Disk-based) 太慢且連線昂貴,我們需要一個比它快 100 倍、甚至 1000 倍的東西來擋在前面。這個東西就是 記憶體快取 (In-Memory Cache),最經典的代表是 Redis。
利用 Redis 的 原子操作 (Atomic Operations) 來管理庫存。Redis 是 單執行緒 (Single-threaded) 的,這聽起來像是缺點,但在計數場景下是巨大的優點:它天生就保證了操作的順序性,而且完全不需要像資料庫那樣複雜的鎖機制。
在搶票時,我們直接設一個閘門:「庫存剩 100 張,每過一個人就減 1,減到 0 就關門。」
Redis 就負責這個快速的通關過程,剩餘的業務邏輯則是放在後面由資料庫慢慢處理。
### 技術細節
Redis 提供了一個指令叫做 DECR (Decrement,遞減)。
* 原子性:即使 10 萬人同時送出 DECR ticket_count,Redis 會把它們排成一直線,一個接一個處理(因為它是單執行緒)。
* 速度:Redis 每秒可以處理 10 萬到 50 萬次 操作(取決於硬體),比關聯式資料庫快得多。
#### 具體流程
* 預熱 (Pre-heat):開賣前,先把票數寫入 Redis:SET ticket_1_stock 10000。
* 搶票:
* 使用者請求進來。
* 應用程式直接呼叫 Redis:DECR ticket_1_stock。
* Redis 回傳剩餘數值。
* 判斷:
* 如果回傳值 >= 0:搶票成功! (雖然還沒寫入資料庫,但我們先認定他拿到了)。
* 如果回傳值 < 0:票沒了! 直接回傳「售完」。
### 權衡 (Trade-offs)
這是不是完美的?
優點 (Pros):
* 極致效能:完全擋住了資料庫的壓力。流量在 Redis 這一層就被消化掉了。
* 無鎖:沒有複雜的 Lock 等待機制,Redis 處理完一個馬上處理下一個。
缺點 (Cons):
* 資料一致性風險:Redis 是記憶體資料庫,萬一 Redis 剛扣完庫存(顯示剩 99 張),結果伺服器突然斷電,而這個「扣庫存」的動作還沒同步回真正的資料庫 (PostgreSQL/MySQL),怎麼辦?這會導致 「少賣 (Under-selling)」 或 「庫存不準」。

這就是現代高併發系統的標準起手式:「Redis 擋前面,資料庫躲後面」。
### 問題
如果在 Redis 這一層,我們判斷使用者 搶票成功 了(Redis 庫存扣了,變 99),我們直接告訴使用者「恭喜!請付款」。 但是,真正的訂單資料還沒寫進 資料庫 (MySQL/PostgreSQL) 裡。
如果這時候,應用程式伺服器 (App Server) 突然當機了,或者網路斷了一下,導致這個「寫入資料庫」的動作失敗了。
1. Redis 已經扣掉一張票了 (剩 99)。
2. 資料庫卻沒有這筆訂單。
請問這會造成什麼後果?這張票發生了什麼事?
----
## 訊息佇列 (Message Queue)
我們不能讓「扣庫存」和「寫訂單」這兩件事硬綁在一起(同步執行)。因為如果寫訂單太慢或失敗,就會卡住或遺失前面的操作。
我們需要一個緩衝區。
訊息佇列 (Message Queue, MQ) 是一個「非同步的通訊機制」。它就像是一個郵筒或輸送帶。 生產者 (Producer) 把訊息丟進去就走(不等人);消費者 (Consumer) 有空時再來慢慢拿出來處理。
常見的技術有 Kafka, RabbitMQ, Amazon SQS。
用餐飲櫃台舉例:
* 櫃台 (Redis層):店員只負責收錢、給號碼牌 (Ticket)。動作極快。
* 杯子隊列 (MQ):店員把寫著飲料名的杯子排在咖啡機上方的軌道上。
* 咖啡師 (資料庫層):依照自己的節奏,拿一個杯子、煮一杯。就算咖啡師動作慢,櫃台還是可以一直收錢。
### 技術細節
在 Ticketmaster 的架構中,MQ 的作用是 「削峰填谷」。
* 搶票瞬間 (Peak):10 萬人湧入
* Redis 擋前鋒:快速檢查庫存,發放「購買資格」。
* 丟入 MQ:只要 Redis 說有票,App Server 就把「User A 買到了 Ticket #1」這個訊息,極快地 丟進 MQ (Kafka)。
* 這動作只需幾毫秒,而且 MQ 保證訊息不遺失 (Persistence)。
* 慢慢寫入 DB:後端有一群 Worker (工人程式) 慢慢從 MQ 拿訊息,一筆一筆穩穩地寫入 MySQL/PostgreSQL。
* 就算資料庫一秒只能寫 1000 筆,MQ 可以堆積 10 萬筆訊息慢慢消化,不會讓系統崩潰。
### 權衡 (Trade-offs)
MQ 是救星,但也帶來了麻煩。
* 優點 (Pros):
* 解耦 (Decoupling):下單 (Redis) 和 記帳 (DB) 分開。
* 削峰 (Traffic Smoothing):保護脆弱的資料庫不被瞬時流量沖垮。
* 保證傳遞:即使資料庫掛了,訊息還在 MQ 裡,修好後繼續跑即可,解決了資料遺失問題。
* 缺點 (Cons):
* 系統複雜度暴增:你要多維護一個 Kafka 集群。
* 最終一致性 (Eventual Consistency):
* 使用者在前端看到「搶購成功!」,但其實資料庫可能 5 秒後 才真的寫入訂單。
* 萬一這中間 MQ 發生災難性故障,或者 Worker 寫入失敗(例如信用卡扣款失敗),你必須設計「補償機制 (Compensation)」把 Redis 的庫存加回去。這邏輯寫起來非常痛苦。
### 問題
當使用者點擊「購買」...
請求先到達 _________,檢查並扣除庫存。
如果成功,應用程式立刻發送一條訊息給 _________。
此時,我們可以先回傳給使用者什麼訊息? _________ (是「購買成功」還是「排隊中」?)
後台的 Worker 從 _________ 拿出訊息,執行 _________ 操作。
----
## 倒排索引 (Inverted Index) 與 全文檢索
在搶票之前,使用者得先「搜尋」。 如果你的資料庫有 100 萬場活動,使用者搜尋 "Taylor Swift"。
* SQL 的做法:SELECT * FROM events WHERE description LIKE '%Taylor Swift%'。
* 後果:這是「全表掃描 (Full Table Scan)」。資料庫會從第一行看到最後一行。如果有 1000 萬筆資料,這行指令會跑好幾秒甚至超時 (Timeout),系統直接卡死。
倒排索引 (Inverted Index) 是一種專門為了「搜尋關鍵字」設計的資料結構。它不存「哪一行有什麼字」,而是存「這個字出現在哪幾行」。
最常用的技術是 Elasticsearch (ES)
* SQL (正向索引):就像你要找書裡哪一頁提到了「哈利波特」。你必須從第 1 頁翻到第 500 頁,一頁一頁看。
* Elasticsearch (倒排索引):你直接翻到書最後面的「索引頁 (Index)」。
* 找到 "H" 開頭 -> "Harry Potter" -> 旁邊寫著頁碼:[3, 42, 108]。
* 你直接翻到這三頁,速度快幾千倍。
想起被資料結構跟作業系統支配的恐懼了......
### 技術細節
當我們把活動資料存入 Elasticsearch 時,它會進行 斷詞 (Tokenization):
* 原始句子:"Taylor Swift World Tour"
* ES 建立索引:
* "taylor" -> 指向 Event ID: 101, 105
* "swift" -> 指向 Event ID: 101, 105
* "tour" -> 指向 Event ID: 101, 200
當使用者搜尋 "Swift Tour" 時,ES 只要取這兩個清單的交集 (Intersection),立刻就能給你 ID 101,耗時通常在毫秒 (ms) 等級。
### 權衡 (Trade-offs)
這又是架構師最頭痛的地方。我們現在有兩個資料庫:
1. PostgreSQL (主資料庫):存訂單、存票務,講求 ACID,資料絕對正確。
2. Elasticsearch (搜尋引擎):存活動內容,講求搜尋速度。'
問題: 當後台管理員在 PostgreSQL 修改了演唱會時間,Elasticsearch 怎麼知道?如果兩邊資料不一樣,使用者搜尋到了,點進去卻發現時間不對(資料不一致)。
有兩種常見解法:
* 解法 A:應用層雙寫 (Application Dual Write)
* 程式碼寫:db.updateEvent(); es.updateEvent();
* 缺點:萬一 DB 成功了,ES 失敗了怎麼辦?資料就爛掉了。
* 解法 B:變更資料擷取 (Change Data Capture, CDC) —— 推薦方案
* 原理:我們不讓應用程式直接寫 ES。我們去監聽 PostgreSQL 的「交易日誌 (Transaction Log / Binlog)」。
* 只要 DB 一有變動,CDC 工具 (如 Debezium) 就會抓到這個訊號,自動把變更同步到 ES。
* 優點:解耦,保證最終一致性。即使 ES 掛了,修好後 CDC 會繼續重播日誌,資料不會掉。

### 問題
現在已經解決了「搶票 (Redis/MQ)」和「搜尋 (Elasticsearch)」。
關於 「使用者體驗 (UX)」 與 「系統資源」又會有什麼樣的問題?
使用者小明進到了「選位頁面 (Seat Map)」,他看到第一排有一個空位(綠色)。 他猶豫了 5 秒鐘,正在思考要不要買。 就在這 5 秒內,另一個使用者小華手快,把那個位子買走了。
如果是傳統的網頁 (HTTP Request),小明的畫面不會變,還是顯示綠色。 當小明開心地按下「購買」時,系統會跳出無情的錯誤:「抱歉,位子沒了」。這體驗非常差。
你會用什麼技術來解決這個問題? 讓小明的畫面在小華買走的那一瞬間,那個綠色的格子自動變成紅色(已售出)?
----
## WebSockets vs. SSE
WebSockets 是一種在單個 TCP 連線上進行 「全雙工 (Full-Duplex)」 通訊的協定。
* 全雙工:意思就是「雙向道」。伺服器可以主動推播資料給客戶端,客戶端也可以隨時傳資料給伺服器,兩邊可以同時說話。
* 持久連線 (Persistent Connection):不像 HTTP 講完話就掛電話,WebSockets 是電話接通後就不掛斷,直到一方主動掛掉。
之前的文章有寫到過 WebScokets 跟 HTTP,這裡就簡單帶過,總之,WebScokets可以做到即時更新!
#### 座位圖變色 (The Green-to-Red Magic)
讓我們看看小華買票瞬間,系統內部發生了什麼事:
* 小華 (User B) 買了位子,Redis 扣庫存成功。
* 後端 (Backend) 發布一個事件到 Redis Pub/Sub (發布/訂閱系統):「注意!座位 #5 狀態變更為『已售出』」。
* WebSocket 伺服器 訂閱了這個頻道,收到了這個消息。
* WebSocket 伺服器 找到所有正在看這個座位圖的使用者 (包含小明),透過他們各自建立的 WebSocket 連線,廣播這條消息。
* 小明 (User A) 的瀏覽器收到封包,JavaScript 執行,將座位 #5 的 CSS class 從 green 變成了 red。
* 這一切都在毫秒級內完成,小明還沒來得及點,格子就變紅了。
雖然 WebSockets 很強,但在「座位圖更新」這個特定場景下,其實有一個更輕量級的對手:Server-Sent Events (SSE)。
SSE 是 單向 (Uni-directional) 的。就像收音機廣播,只有電台 (伺服器) 能說話,聽眾 (瀏覽器) 只能聽。
可以理解成 WebSockets 適合用在多人互動的場景(聊天室),SSE則適合用在單向場景(股票報價、座位圖更新)
----
## 擴展性 (Scalability) 與 虛擬排隊 (Virtual Queue)
到目前為止,我們已經設計出一個能運作的系統了。 但如果這時候,不是 10 萬人,而是 100 萬人 甚至是 1000 萬人 同時湧入(例如周杰倫引退演唱會)。
即使你有 Redis,你有 Kafka,你的前端網頁伺服器 (Web Server) 可能連「建立連線」都來不及,就會被瞬間的流量 DDoS 打掛。
#### 虛擬排隊系統 (Virtual Waiting Room)
這就是你在熱門演唱會開賣時看到的那個「轉圈圈小人」畫面:「目前排隊人數 50,000 人,預計等待時間 30 分鐘」。
### 技術細節
這不是一個靜態頁面,這是一個 流量控制閥 (Rate Limiter)。
* 使用者連線進來,還沒碰到主系統 (Redis/DB)。
* 先拿到一張 號碼牌 (Token)。
* 系統檢查:「現在主系統只能承受 5,000 人同時在線」。
* 如果你的號碼牌在 5,000 號以內 -> 放行 (Redirect) 到選位頁面。
* 如果你的號碼牌是 50,001 號 -> 擋住,顯示排隊頁面,每 10 秒檢查一次前面的隊伍消化了沒。
* 這能確保後端的 Redis 和資料庫永遠只面對「固定數量」的敵人,而不是一次面對百萬大軍。
----
前端 (Frontend):使用 WebSockets (或 SSE) 接收即時座位圖更新,避免使用者買到被搶走的票。
閘道層 (Gateway):使用 虛擬排隊 (Virtual Queue) 擋住瞬間流量,保護系統。
庫存層 (Inventory):使用 Redis + 原子操作 來處理高併發扣庫存,解決「超賣」問題。
緩衝層 (Buffer):使用 Kafka (Message Queue) 將訂單非同步傳送,解決「寫入瓶頸」問題。
持久層 (Persistence):使用 PostgreSQL (ACID) 確保訂單最終安全寫入,解決「資料遺失」問題。
查詢層 (Search):使用 Elasticsearch 提供極速的關鍵字搜尋。
最後問題:
「你的 Redis 設計是用 DECR 扣庫存,看起來很完美。但是,萬一 Redis 伺服器本身突然燒掉了(物理損壞),雖然我們有資料庫做備份,但中間那幾秒鐘的庫存數據(Redis 裡的 99 張 vs 資料庫裡的 100 張)可能會有落差。你要怎麼在 Redis 重啟後,快速讓庫存恢復準確?」
首先,去問資料庫
* 詢問:「目前已經確認寫入的訂單有幾筆?」
* 回答:「我看帳本,已經賣掉 80 張。」
Kafka 是補償 (The In-Flight Transactions)
資料庫不知道那些「剛剛在 Redis 搶到,但還在排隊寫入」的訂單。
如果我們忽略 Kafka,直接把庫存設為 100 - 80 = 20,那這 20 張票可能會被重複賣給別人 (超賣),因為其實有 5 個人已經在 Kafka 排隊了。
* 詢問:「Kafka 隊列裡還有多少筆『已搶到票』的訊息還沒被 Worker 消化?」
* 回答:「還有 5 筆訂單正在排隊。」
新的 Redis 庫存 = 總票數 (100) - 資料庫已存 (80) - Kafka 排隊中 (5) 結果 = 剩餘 15 張。
我們把 15 這個數字寫入新的 Redis (SET ticket_stock 15),系統就可以重新開放搶票,而且保證不會超賣。
或是用Redis AOF (Append Only File)
* 這是 Redis 的黑盒子記錄器。它會把每一個執行過的指令 (DECR, SET) 都寫進硬碟的一個日誌檔。
----
## 結論
### 壞設計:Cron Job (排程任務)
* 做法:在資料庫加一個欄位 status='reserved' 和 time=12:00。然後寫一個背景程式 (Cron Job),每 10 分鐘跑一次 SQL:UPDATE tickets SET status='available' WHERE time < now() - 10min。
* 為什麼爛? (The Critique):
* 時間差 (The Gap):Cron Job 是週期性的。如果你設定每 10 分鐘跑一次,而在 12:09 有張票過期了,Cron Job 可能要等到 12:10 甚至 12:19 才跑。這張票會被「多鎖住」好幾分鐘,導致別人無法購買。
* 資料庫負擔:全表掃描 (Full Table Scan) 檢查過期訂單,對資料庫壓力極大。
### 好設計:Redis Key Expiration (TTL)
* 做法:利用 Redis 的 TTL (Time To Live) 功能。
* 指令:SET ticket_1_owner UserA EX 600 (設定這張票屬於 UserA,600秒後自動刪除)。
* 機制:Redis 會自己倒數。時間一到,這個 Key 自動消失。
* 檢查邏輯:
* 使用者要買票 -> 查 Redis。
* 有 Key? -> 代表被保留中(不管付款沒)。
* 沒 Key? -> 代表可購買。
這才是處理「暫時保留 (Temporary Reservation)」最優雅的方式,完全不需要寫程式去掃描資料庫。
### 讀寫分離與快取策略 (Read/Write Split & Caching)
一個 讀多寫少 (Read-Heavy) 的系統。可能有 100 萬人在看演唱會資訊,但只有 1 萬人在買票
我們之前的設計太專注於「買票 (Write)」,忽略了「看資訊 (Read)」的優化。
#### 1. CDN (Content Delivery Network)
* 靜態資源:演唱會的海報圖片、座位圖的底圖 (SVG/PNG)、CSS/JS 檔案。
* 策略:全部丟到 CDN (如 Cloudflare, AWS CloudFront)。讓使用者從離家最近的節點下載,而不是連回你的主機。這能減少 90% 的頻寬消耗。
#### 2. 快取層 (Cache Aside Pattern)
* 動態資訊:演唱會標題、演出者介紹、地點資訊。這些資料幾乎不會變。
* 策略:
* App 先問 Redis:「有五月天演唱會的介紹嗎?」
* 有 -> 直接回傳 (1ms)。
* 沒有 -> 去資料庫查 (10ms) -> 寫入 Redis -> 回傳。
* 失效策略:設定 24 小時過期,或是當管理員修改資訊時主動刪除 Cache。
#### 3. 資料庫分片 (Sharding)
當你的 Ticketmaster 成長到全球規模,單一台 PostgreSQL (即使有 Read Replica) 也存不下所有訂單時,文章提到需要做 Sharding (分庫分表)。
#### 該用什麼欄位來分片 (Partition Key)?
* 選項 A:用 User ID 分片
* 做法:User ID 1~10000 的訂單在 DB_1,10001~20000 在 DB_2。
* 缺點:查詢「某場演唱會賣出多少票」時,你需要去問所有的 DB,效率極差。
* 選項 B:用 Event ID 分片 (推薦)
* 做法:五月天 (ID 100) 的所有資料在 DB_1,周杰倫 (ID 101) 的在 DB_2。
* 優點:大部分的查詢和搶票都是針對「特定演唱會」的。這樣可以把流量完美隔離。五月天搶票時,DB_1 會很忙,但 DB_2 (周杰倫) 完全不受影響。
