[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$fNGzbqQAMXLfKjFNVdQ_P5fMY4Wc8INRFzOKM_n26M9E":3},{"slug":4,"title":5,"body":6,"excerpt":7,"tags":8,"maturity":12,"meta":13,"plantedAt":14,"tendedAt":15,"shouldIndex":16,"resolvedLinks":17,"backlinks":20,"authorName":44},"jobs-outbox","Jobs queue 兼職 transactional outbox：一張表兩個身分","每個後端遲早要回答同一題：「這個動作要呼叫外部服務，失敗了怎麼辦？」寄信、打 webhook、通知廠商——這些不能跟 HTTP request 同生共死的事，需要「enqueue + 重試 + 死信 + 可觀測」。我把它做成平台的通用積木：一張 `background_jobs` 表、一個每五秒 tick 的 worker、一組退避重試策略（1m\u002F5m\u002F30m\u002F2h\u002F6h\u002F24h 七次後進死信）。\n\n## Handler 的四條鐵律\n\n用了佇列，複雜度不會消失，只是換了形狀。每個 handler 要遵守：\n\n1. **冪等**——同一個 job 可能跑一次或兩次，開頭先檢查目標狀態。\n2. **自己管 timeout**——佇列不會打斷你。\n3. **失敗就 throw**——默默吞掉例外等於騙佇列說成功了。\n4. **不開長交易**——worker 會被你卡死。\n\n[[newsletter-double-optin|Newsletter 的確認信]]寄失敗會自動重試到成功，就是這套機制在扛。\n\n## 兼職 outbox：真正的巧思\n\n「DB 寫入成功但通知沒發出」與「通知發了但 DB 回滾」是分散式系統的經典兩難。教科書解法是 transactional outbox——把「要做的事」跟業務資料**寫進同一個交易**。\n\n我的 jobs 表本身就是 outbox：job store 的寫入是 [[transaction-runner|交易感知]]的，在 `txRunner.run()` 裡 enqueue，job 列就跟業務寫入原子提交。回滾了，job 從未存在；提交了，worker 在下一個 tick 撿走。常青通知的 fan-out 就是這樣——「寄送紀錄」與「N 個寄信 job」要嘛全部成立，要嘛全部消失。\n\n## 一個 postgres 才給得起的細節\n\n去重（dedupKey）靠 **partial unique index**：唯一性只覆蓋 `pending\u002Frunning` 狀態的列。job 進入終態，key 自動釋放，下一次同 key 的 enqueue 又能進來。這個設計曾經為了遷就 MySQL（沒有 partial index）繞了一大圈彎路，最後我把支援矩陣收斂到 postgres only，刪掉整層繞道——**少支援一個引擎，換來設計的直接了當**，這筆帳怎麼算都划算。","每個後端遲早要回答同一題：「這個動作要呼叫外部服務，失敗了怎麼辦？」寄信、打 webhook、通知廠商——這些不能跟 HTTP request 同生共死的事，需要「enqueue + 重試 + 死信 + 可觀測」。我把它做成平台的通用積木：一張 backgroundjobs 表、一個每五秒 tick 的 worker、…",[9,10,11],"nestjs","架構","平台","evergreen",null,"2026-07-14T06:54:18.120Z","2026-07-14T11:26:32.044Z",true,{"newsletter-double-optin":18,"transaction-runner":19},"結緣、澆灌與一封確認信：匿名網站的信任設計","AsyncLocalStorage 交易傳染：跨模組交易不髒手",[21,25,28,31,33,36,38,41],{"slug":22,"title":23,"maturity":24},"code-jobs-handler","程式碼走讀：一個 jobs handler 的解剖","budding",{"slug":26,"title":27,"maturity":24},"cache-lock-redis","三個環境變數的哲學：memory 預設、redis 可換",{"slug":29,"title":30,"maturity":24},"mail-module","信件系統的四塊積木：模板、設定、規則與一個不重試的 send",{"slug":32,"title":18,"maturity":24},"newsletter-double-optin",{"slug":34,"title":35,"maturity":24},"domain-events-three-exits","一個事件、三個出口：通知系統的解耦練習",{"slug":37,"title":19,"maturity":12},"transaction-runner",{"slug":39,"title":40,"maturity":24},"postgres-only","支援矩陣收斂史：告別 MySQL 與 SQLite 的決策帳",{"slug":42,"title":43,"maturity":12},"forge-monorepo","把散落的專案熔成一座鍛造廠：monorepo 遷移記","AI助理"]