2026 年 9 月下旬,一篇發在掘金的技術文章在工程師社羣裡流傳開來,題目很日常:「訂單 30 分鐘未支付自動取消,定時任務為什麼被面試官嫌棄」。這道題幾乎是後端工程師面試的必考題,而文章給出的答案整理了三種替代方案。這件事值得拿出來講,是因為它碰到的情境一點都不小眾:任何做過電商、訂票、外帶預約的工程師,遲早都會撞上「時間到了要自動處理」這個需求。
先看那個被嫌棄的答案長什麼樣。使用者 12:00 下單,系統規則是 30 分鐘沒付款就取消。最直覺的寫法是起一個定時任務,每分鐘去資料庫撈一次:狀態是未支付、建立時間超過三十分鐘的訂單,撈出來逐一取消。程式碼十行寫完,邏輯挑不出錯。
問題在於它笨重。第一個硬傷是時間差。使用者 12:00:00 下單,理論上 12:30:00 就該釋放這筆訂單佔住的庫存與優惠券,但定時任務可能 12:31:00 才輪到它。這一分鐘裡,一個不會付款的訂單持續佔著熱賣商品的庫存。促銷高峯期,這一分鐘足夠讓一個搶手商品少成交幾十單。
第二個硬傷是全表掃描。訂單表長到幾千萬行之後,絕大多數時刻根本沒有過期訂單存在,但任務照樣每分鐘把整張表犁一遍。CPU、磁碟 IO、資料庫連線池,全在為空跑付帳。加索引能減輕一些,但救不了「每分鐘掃一次」這個動作本身的浪費。
文章給出的一句話結論是:把被動輪詢換成基於事件的主動觸發。時間到了自己跳出來,而不是每次都去問一遍「有沒有到期的」。
Redis ZSet:像一條按時間排好的隊伍
第一種替代方案用 Redis 的有序集合(ZSet)。可以把 ZSet 想成一根按時間排好隊的隊伍:每個成員是訂單號,它的分數(score)設定為「下單時間加三十分鐘」。使用者一下單,系統就往隊伍裡塞一筆。
後臺起一個執行緒當檢票員,只盯隊伍最前面那個人:他的到期時間還沒到,就睡一下再來看;到了,就把他領出來執行取消、釋放庫存、退還優惠券。因為隊伍天生按時間排序,檢票員每次只需要看隊首一個,完全沒有掃全表的問題。ZSet 底層是跳表,取最小分數是對數級的開銷,而且隊伍裡只有真正未支付的訂單,不會有無效資料混進來,精確度可以做到秒級。
有個工程細節不能略過:取任務的動作必須原子化。系統部署多個實例時,兩個檢票員同時看到同一筆到期訂單,就會重複取消。實務上用 Lua 腳本把「查詢」和「刪除」打包成一個不可分割的操作,誰刪成功誰負責執行,搶輸的直接放棄。
代價是系統多了一個 Redis 依賴,還得想清楚持久化與服務重啟後的補救掃描。
死信佇列:把「到期」外包給訊息中間件
第二種方案的比喻更像車站的寄物櫃:東西投進去,到點自動彈出來,你只負責在出口接。
具體做法是設兩個佇列。第一個叫緩衝佇列,故意不掛任何消費者,訊息進去時設定 30 分鐘的存活時間(TTL)。使用者下單後,一則訊息被丟進緩衝佇列安靜躺三十分鐘。到期後,訊息中間件判定它變成「死信」,轉送到死信交換機,再路由到第二個業務佇列。真正的消費者在那裡等著,拿到訊息就查訂單狀態、執行取消。
這套設計最大的好處是業務程式碼乾淨。開發者只管發訊息、收回調,所有延遲邏輯都交給中間件,團隊不用自己養檢票員執行緒。
但坑也在這裡。這套機制成立的前提,是同一個緩衝佇列裡所有訊息的延遲都一樣長。一旦把 5 分鐘、10 分鐘、30 分鐘的超時混進同一個佇列,先進去的長延遲訊息會把後面短延遲訊息的實際等待時間硬生生拉長,這是隊頭阻塞。想做多級延遲,就得為每種時長建一組獨立佇列,或者改用原生支援延時訊息的 RocketMQ。
時間輪:不靠外部依賴,單機扛幾十萬個計時器
面試官若再追一句「不依賴 Redis 也不依賴訊息佇列,單機記憶體裡怎麼處理幾十萬個延時任務」,標準答案會轉向時間輪。
可以想像一塊機械秒錶:錶面上六十個刻度代表六十秒,指針每秒走一格。每個刻度掛著一串任務,指針走到哪格,就把那格掛的任務全部觸發。想把時間拉長,就再加一層外圈,像時針與分針的關係,低層轉完一圈才推動高層走一格。任務的掛載與觸發都是常數時間的開銷,這讓它成為 Netty、Kafka 這類框架內部處理大量逾時連線的標配工具。限制同樣明顯:資料在記憶體裡,程序重啟就歸零,任務量再大到需要多機分擔時,它就不是首選了。
所以呢:工程選擇是對帳單,不是對錯題
回到一開始的面試場景。定時任務不是錯,在訂單量小、精確度要求鬆的場景裡,它甚至是成本最低的正確答案。面試官嫌棄的點在於候選人沒意識到自己選的方案附帶什麼帳單:每一分鐘的空轉掃描、每一筆被白白佔住的庫存,都是規模上去之後要付的利息。
這個思路對非工程背景的讀者也適用。日常工作中許多「每小時檢查一次信箱有沒有新信」「每天早上逐一確認任務有沒有逾期」的流程,本質上都是輪詢:絕大多數檢查是空跑,真正的事件往往在兩次檢查之間就過了有效期限。把「定期問一遍」改成「事件發生時通知我」,從電子郵件的自動轉寄規則、協作工具的觸發器到行事曆提醒,都是同一個原則的日常版本。
技術選型的核心動作,是把每個方案的帳單攤開來對:ZSet 帳單是多一個 Redis 依賴與持久化考量;死信佇列帳單是延遲時長必須分隊列管理;時間輪帳單是單機記憶體、重啟歸零。沒有免費的方案,只有把成本放在哪裡的選擇。能把帳單講清楚的工程師,才是面試官真正想留下來的人。
主題