先做個小測驗:你們 codebase 裡快取的 TTL 是多少?為什麼是那個數字?

第一題大家都答得出來(300,或 3600)。第二題通常換來一陣沉默——因為那個數字是某年某月某人隨手填的,之後所有人照抄。TTL 就這樣變成整個快取系統裡最重要卻最沒人想過的參數。

它重要在哪?因為 TTL 同時是三件事:髒資料的最長壽命(cache 跟 DB 不一致時,TTL 是自我修正的保險絲)、命中率的調節閥(太短就一直 miss,快取形同虛設)、記憶體的水位控制(沒有 TTL 的快取只進不出)。訂錯任何一頭都會痛,只是痛法不同。


底線:當快取用,就一定要有 TTL

比「訂多長」更優先的是「有沒有」。沒設 TTL 的快取 key 是永生的:資料改了它不會自己好(一致性策略再完備,總有漏網的 race,TTL 是最後一道保險)、記憶體只進不出(等著撞 maxmemory 看淘汰策略臉色)。

「這個 key 我會記得手動清」——不,你不會。回填快取的那行 code 就把 TTL 帶上,讓它成為肌肉記憶而不是判斷題。


三種訂法

一、固定 TTL——起點,但別停在這。 一個數字走天下。訂多長的思考框架其實只有一句:這份資料髒掉多久是可以忍受的? 商品名稱髒五分鐘沒人會死;剩餘庫存髒五分鐘可能就超賣。從「業務能忍多久」倒推,而不是從「大家都寫 300」開始。

二、固定 TTL 加 jitter——批量寫入的必需品。 一次回填一萬個 key、全部同一個 TTL,等於預約了一場五分鐘後的集體過期——那一秒的 miss 海嘯全部灌進 DB(雪崩)。解法一行:TTL 加隨機零到六十秒,把到期時間打散。成本趨近於零,擋掉一種事故。

三、跟著業務走的動態 TTL。 不同資料忍髒的程度天差地遠,那 TTL 就不該是同一個數字:熱門商品頁可以長一點(配寫時刪除,髒的窗口本來就短)、搜尋結果短一點(本來就允許略舊)、庫存這種敏感的極短甚至不快取。實作上就是 CacheService 的 set(key, val, ttl) 讓呼叫端按資料類型覆寫預設值——機制半小時就能加好,難的是逼自己回答每類資料「能忍多髒」。


滑動過期:session 那一掛的專用款

上面講的都是「寫入時決定、時間到就死」的固定式過期。還有一種語意完全不同的:每次被用到就重新續命——連續活動就一直活著,閒置夠久才過期。這是 session、購物車這類「跟使用者活躍度綁定」的資料的天然模型。

Redis 上的實作很直接:每次讀取後 EXPIRE 重設回滿額,或用 GETEX 一個指令同時取值加續命。判斷要不要用它的方法也簡單:問自己「這份資料的生命週期,是跟時間走、還是跟使用者的活躍走?」後者就是滑動過期的場子。


兩個不報錯、但會咬人的坑

SET 覆蓋會把 TTL 清掉。 一個 key 原本有過期倒數,你單純想更新它的值、裸 SET 下去——TTL 沒了,這個 key 從此永生。Redis 一聲都不會吭。要保留倒數,明確帶 KEEPTTL;本來就想重新計時,就明確帶新的 EX。總之更新有 TTL 的 key 時,別裸 SET

TTL 的回傳值有三種,別看錯。 查一個 key 的 TTL:正整數是剩餘秒數、-1 是「key 在、但沒設過期」、-2 是 key 不存在。排查「這 key 怎麼還在」的時候,-1 就是兇手的自白——它是永生的。

我的 TTL 學費在02 的翻車帳單攤過總帳,這裡只切 TTL 這一刀:當年最痛的兩課,都不是「數字訂錯」,是根本沒把 TTL 當一個需要設計的東西。第一課,多處快取的 TTL 訂在整齊重複的時間點,同一瞬間集體過期——上面「加 jitter 成本趨近於零」那句,我是用一次 DB 被打爆換來的,一行隨機數的事。第二課,查無資料的空值跟正常資料吃同一條 TTL,爬蟲掃過的每個「不存在」都活得跟真資料一樣久,錢包先於監控發現這件事——空值的 TTL 要短得多,這個坑在穿透那篇會展開。兩課的共同結論就是本文主旨:TTL 不是一個數字,是每一類資料都要各自回答的設計題。


收束成一個決策順序:先保證「有」TTL → 長度從「業務能忍多髒」倒推 → 批量寫入加 jitter → session 類改用滑動過期 → 更新值時記得 KEEPTTL。 五步都不難,難的是把「TTL 是設計決策、不是魔法數字」這個意識裝進團隊腦袋裡。

(TTL 撐不住的場景——熱點 key 過期瞬間的擊穿——見同系列 #18;TTL 之外的主動失效手段,見 invalidation 各篇。)

接下來往哪走