TL;DR | 面試情境模擬 #
👴 面試官:Redis 和 SQL 同時使用時,資料要怎麼同步?哪些場景要設 TTL,哪些不能設?
🧑💻 你:核心是先判斷資料屬性:短期狀態可設 TTL,核心資產不能隨便過期。Session 適合用 Redis TTL;排行榜與共同好友通常 SQL 保存真實資料、Redis 負責即時查詢;瀏覽數用 Redis 抗高併發,再由 Worker 批次回寫 SQL;購物車屬於用戶資產,不應短 TTL,通常結帳時才同步成訂單。
情境一:使用者登入(Session) #
核心概念:臨時狀態驗證,需保證consistency。
📊 data-flow (Data Flow) #
- 寫入時 (Write):
- MySQL:執行
INSERT INTO sessions,作為登入紀錄的最終真相(Source of Truth)。 - Redis:執行
SETEX session:1001 3600 "data"。利用 TTL (Time To Live) 設定過期時間,時間一到自動刪除,無須手動清理。
- MySQL:執行
- 讀取時 (Read):
- 先查 Redis
GET session:1001。若有(Cache Hit)直接驗證;若無(Cache Miss)則查 MySQL 並回寫 Redis。
- 先查 Redis
- 同步策略 (Logout):
- 使用者登出時,先刪除 Redis Key,再更新 MySQL 狀態。
情境二:遊戲排行榜(Leaderboard) #
核心概念:Sorted Set (ZSet) 的跳躍表結構。
🤔 為什麼 Redis 能「即時排序」? #
- MySQL 瓶頸:
ORDER BY是「事後排序」,數據量大時需要全表掃描與排序,效能極差。 - Redis 優勢:底層使用 Skip List (跳躍表)。執行
ZADD時,Redis 會根據分數 (Score) 直接將成員插入到正確位置,維持寫入即有序。
📊 data-flow #
- 寫入時 (Dual Write):
- MySQL:
UPDATE user_scores SET score = 9550(永久保存紀錄)。 - Redis:
ZADD game:rank 9550 "user_1001"(即時更新排名)。 - 註:此類高頻變動數據通常「以 Redis 為準」顯示,MySQL 僅作備份。
- MySQL:
- 讀取時:
ZREVRANGE game:rank 0 9:直接從跳躍表截取前 10 名,O(log N) 複雜度,極快。
情境三:文章瀏覽數(高併發寫入) #
核心概念:Redis 扛流量,後台排程 (Worker) 批次回寫。
🤔 為什麼要用 Worker 與 SCAN? #
- Worker (背景任務):一個在伺服器後台定期運行的腳本(如每 5 分鐘一次)。負責將 Redis 的暫存數據同步至 MySQL,避免前端請求直接阻塞database。
- SCAN 指令:
- 使用
SCAN 0 MATCH article:*:views COUNT 100。 - COUNT 100 代表「分批提示」,告訴 Redis 每次只掃描約 100 筆 Key。因為 Redis 是單執行緒,若用
KEYS一次性遍歷所有 Key 會導致服務卡頓 (Blocking),SCAN則能非阻塞地分批處理。
- 使用
📊 data-flow #
- 寫入時 (高併發):
- Redis:
INCR article:888:views(原子操作,無鎖累加)。 - MySQL:不操作。避免高併發下的行鎖 (Row Lock) 效能瓶頸。
- Redis:
- 同步時 (定時):
- Worker 讀取 Redis 值 (如 500),解析文章 ID。
- 執行 SQL:
UPDATE articles SET view_count = view_count + 500 WHERE id = 888。- 註:這是標準 SQL 語法,database會在內部讀取舊值加上 500 並寫入,避免程式計算的時間差問題。
- 重置 Redis:執行
SET article:888:views 0。- 註:必須重置。若不重置,下次會將「舊數據」再次累加進 MySQL,導致雙倍計數。
情境四:共同好友(集合運算) #
核心概念:Set 交集運算,取代複雜 SQL 子查詢。
🤔 為什麼比 SQL JOIN 快? #
若要找出 Alice (1001) 和 Bob (1002) 的共同好友:
- SQL 做法:需要用到子查詢或
IN語法,當好友列表龐大時,查詢效率會隨資料量下降。-- 找出 Alice 的好友,且該好友也在 Bob 的好友列表中 SELECT friend_id FROM friendships WHERE user_id = 1001 AND friend_id IN (SELECT friend_id FROM friendships WHERE user_id = 1002); - Redis 做法:
SINTER user:1001:friends user:1002:friends。直接在記憶體中計算數學交集,速度極快。
📊 data-flow #
- 寫入時 (雙向寫入):
- MySQL:
INSERT INTO friendships ...(建立關係)。 - Redis:應用程式需同時更新雙方的 Set (雙寫)。
SADD user:1001:friends "1002" -- 把 Bob 加入 Alice 的好友圈 SADD user:1002:friends "1001" -- 把 Alice 加入 Bob 的好友圈
- MySQL:
- 讀取時:
SINTER user:1001:friends user:1002:friends-> 瞬間回傳共同好友 ID 列表。
情境五:購物車(狀態persistence) #
核心概念:頻繁局部修改,不應設 TTL。
🤔 為什麼購物車不能過期? #
購物車屬於用戶資產。使用者可能幾天後才結帳,若設 TTL (如 1 小時),過期後購物車清空會嚴重影響體驗。實務上通常不設 TTL 或設定極長的過期時間。
📊 data-flow #
- 寫入時:
- Redis:
HSET cart:1001 product_A 3。利用 Hash 結構,可直接修改單一商品數量,無需讀取整個購物車。 - MySQL:暫時不寫。購物車屬於「中間狀態」,頻繁寫入database成本過高。
- Redis:
- 讀取時:
HGETALL cart:1001。
- 同步時 (結帳觸發):
- 使用者點擊結帳 -> 讀取 Redis 內容 -> 生成訂單寫入 MySQL -> 刪除 Redis
cart:1001。
- 使用者點擊結帳 -> 讀取 Redis 內容 -> 生成訂單寫入 MySQL -> 刪除 Redis
💡 總結:一眼看懂data-flow與同步策略 #
| 情境 | Redis 結構 | 為什麼用 Redis? | 數據同步策略 (Sync Strategy) |
|---|---|---|---|
| Session | String (Key-Value) | TTL 自動過期,加速驗證 | Cache Aside:登出時刪 Redis,保持與 DB 一致。 |
| 排行榜 | ZSet (跳躍表) | 寫入即排序,無需事後 ORDER BY |
雙寫:MySQL 存紀錄,Redis 負責即時顯示。 |
| 瀏覽數 | String (Counter) | 原子操作 INCR,無鎖抗高併發 |
Worker 批次回寫:定時累加至 MySQL 後重置 Redis,不設 TTL。 |
| 共同好友 | Set (集合) | 記憶體交集運算遠快於 SQL 子查詢 | 雙寫 (雙向):新增好友時需同時寫入雙方的 Set。 |
| 購物車 | Hash (字典) | 支援局部修改 (HINCRBY),無需讀寫整包 |
結帳觸發:平時只存 Redis,結帳時寫入 MySQL 並清空。 |