""

想像一條高速公路,如果有太多交流道、紅綠燈、車子搶道,雖然看起來能同時容納很多車,但實際上常常塞爆。Redis 的設計哲學剛好相反:它選擇了一條「單行道」的路線──簡單、專注,一次只處理一件事。

因為它把所有資料放在記憶體裡,所以車子(請求)一旦進來,就能以極快的速度通過。這種設計讓 Redis 在需要低延遲的場景裡,成為最可靠的快取層。它不追求資料永遠存在(Persistence),而是保證當下的快與穩。

🍂 Redis 作為快取的典型流程(Cache-aside)

一般來說我們會採用「讀取型快取」(Cache-aside)流程:

  1. 使用者要看商品列表(GET)
  2. 系統先去 Redis 看有沒有這筆快取資料
  • 有的話(hit):直接回傳
  • 沒的話(miss):去主資料庫拿資料 ➡ 存進 Redis(快取) ➡ 回傳結果

這種情況 Redis 就算整個掛了也沒差,系統還是能從主資料庫把資料拿回來,最多只是變慢一點(因為每次都去查資料庫)



🍂 Redis 分散式架構(Sharding)

不同的 Key/Value 會放到不同的 Redis 機器上,除非你只有一台 Redis(Single-node),就是用「Sharding」(分片)機制,把不同的 key 分配到不同機器上。由系統(Redis Cluster)用「Hash」的方式計算

比如:

hash(“user:1”) % 3 = 0 ➜ 放 Redis A
hash(“user:2”) % 3 = 1 ➜ 放 Redis B

這個過程對使用者來說是透明的,你不用管它怎麼分配,它自己會分配。



🍂 併發問題與 Optimistic lock

假設商品庫存存在 Redis:

1
2
3
4
5
product:123
{
"stock": 10,
"lastUpdateTs": "2025-08-17T10:00:00"
}

同時有兩個使用者要買這個商品,各自:

  • 從 Redis 讀到 stock=10
  • 決定扣掉 1 個 ➜ 準備寫成 stock=9

兩人幾乎同時送出,結果可能都改成了 stock = 9(最後一個覆蓋前一個) ➜ ⚠️ 錯了!應該要變成 stock = 8

所以 Redis 上用 Optimistic Lock 的解法是,使用 Redis 的 Transaction(MULTI / EXEC)搭配 WATCH,並搭配我們自定義的 LastUpdateTs 來實作 Optimistic Lock !

我們用一個 Redis Hash 來儲存資料:

HSET product:123 stock 10
HSET product:123 lastUpdateTs 2025-08-17T10:00:00

更新流程(使用者想要扣掉 1 個庫存):

1
2
3
4
5
6
7
8
1. WATCH product:123 (監控這個 key,有沒有被改動)
2. HGETALL product:123 拿出目前資料 (讀取這個 Hash 的所有欄位值)
3. 檢查 lastUpdateTs 是否等於你預期的值
4. 如果是 ➜ MULTI (開始一個交易 Transaction)
HINCRBY product:123 stock -1 (對 Redis 的 Hash 中某個欄位的值進行加減整數運算)
HSET product:123 lastUpdateTs [現在時間] (設定一筆 Hash 資料中的欄位值)
EXEC(提交)
5. 如果不是 ➜ 取消修改,重新再來


🍂 Single Thread

Redis 實例(node)只有一條執行緒處理所有指令,所有 key 都共用一條執行緒,所以需要用 Redis Cluster 水平切分資料(sharding),每個 node 各處理一部分 key

如果同一筆資料只有一個 Key

如果只有一個 key(Hot Key):

1
GET homepage:popularProducts

所有人都打這個 key ➜ 都會排在同一個 Redis node、同一條執行緒、同一把 key 的排隊序列上。

結果會太多人同時 GET 同一個 key 雖然每次很快,但累積多了還是會排隊,同時有 SET、更新這個 key 更慘,讀寫交錯,會被序列化處理(每次只能做一件事),如果還要重建快取,重建資料過程中的 SET 會卡住其他讀取的 GET

改成多個 key(分流)

1
2
GET homepage:popularProducts:groupA
GET homepage:popularProducts:groupB

把資料切成多份(ex: 分類、區塊、隨機)

前端同時送出多個 GET(或依需求載入),Redis 處理多個 key,可以:

  • 落在不同 node(在 Redis Cluster 中會自動分配 key)
  • 就算在同一 node,也讓每個 key 處理的請求數量降低

Why Single Thread

Redis 故意做成單執行緒,是為了「簡單、高效、穩定」,並不是限制,而是設計哲學。

✅ 理由 1:避免多執行緒的複雜與效能成本

多執行緒要解決的最大問題是:

多個 thread 同時讀寫一塊資料 ➜ 容易衝突 ➜ 就要「加鎖(mutex)」,而加鎖會導致 效能下降 + 程式複雜度爆炸 + 死鎖風險

Redis 的選擇是:

乾脆整個核心用單執行緒來處理命令,所有操作都序列化排隊執行
➜ 就不會有資料同步問題,也不用加鎖!


✅ 理由 2:記憶體操作本來就快,不需要多執行緒

Redis 是 in-memory 資料庫,所有資料都在記憶體裡
➡ 所以「一次處理一筆資料」其實非常快(單筆常常是 微秒 級)

如果你寫入、讀取一次只要 0.2 毫秒,那 1 秒內就能處理超過 5 千筆請求!

所以單執行緒就夠用,還比多執行緒更穩定!


✅ 理由 3:邏輯簡單,bug 少、效能高

單執行緒的設計讓 Redis 的邏輯非常單純:

  • 每次只處理一件事,不用思考「如果有人同時也在寫呢?」
  • 沒有同步問題、不會 race condition(競爭條件)
  • 也不需要鎖 ➜ 所以效能不會因為加鎖開銷變慢

開發者寫起來簡單,系統效能還比你想像中快


✅ 理由 4:可以靠「Redis Cluster」橫向擴充

Redis 說得很直白:

單一實例(node)是單執行緒沒錯,但你可以用 Redis Cluster 建立多個 node ➜ 每個 node 處理不同的 key ➜ 整體還是可以「平行處理」

Redis 單執行緒 多執行緒系統
一條生產線,專注做一件事 ➜ 不會搞錯、不會搶資源 好幾條生產線,但要花力氣協調,常常搶機器、打架
速度快(因為記憶體超快) 要管誰先做、誰鎖住,成本高
可預測、易維護、穩定 常常要 debug race condition


🍂 Single Thread 還需要 Optimistic Lock 嗎

你會覺得「Redis 是 single-thread,那不是天然就不會衝突了嗎?為什麼還要 Optimistic Lock ?」

事實上,Redis 的 single-thread 保證的是「單一命令」的原子性,但是 無法保證「多步驟流程」的邏輯一致性。 Optimistic Lock (WATCH/MULTI/EXEC)就是用來補這一塊的。

單一命令已經安全 → 不需要 Optimistic Lock

像是:

1
2
3
INCR product:123:stock
HINCRBY product:123 stock -1
SETNX user:1:lock true

這些命令在 Redis 內部一次完成,別的命令插不進來。
所以這種 單命令原子操作 不需要 Optimistic Lock 。


多步驟流程就需要保護

例如「先檢查再扣」的情境:

  1. GET stock -> 10
  2. if stock > 0 then DECR stock

在你的程式碼層面,這是兩個命令:

GET 取值
DECR 扣庫存

Redis 雖然單執行緒,但這兩個命令不是一次送進去的,中間有空檔。如果這時候有另一個客戶端也在操作,就可能都判斷 >0,結果多扣了。這時就需要 WATCH + MULTI/EXEC(Optimistic Lock),確保「我判斷時的值」和「我要更新的值」沒有被別人改過。



🍂 結語

有時候「限制」本身就是一種力量。單執行緒的單行道,不代表落後,反而讓它能在高壓環境中保持可預測與穩定。當快取暫時失效,資料仍然能從資料庫取回;當單一節點撐不住,可以透過 Cluster 擴充。

在這條專注於低延遲的單行道上,Redis 選擇了不與時間賽跑,而是用最純粹的方式,讓每一筆請求,都能快速、安全地抵達終點。