[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"$frFt55LN_3uWvgrWav7zbDo5WCjT0QfqsCSnqrKt7Ai0":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":21},"cache-lock-redis","三個環境變數的哲學：memory 預設、redis 可換","平台的快取與分散式鎖各自抽象成 port，背後兩套實作：in-memory 與 redis，用環境變數切換。這個設計的核心信念是：**fresh clone 必須零外部依賴就能跑起來**。\n\n## 預設 memory 的理由\n\n新人（或三個月後的自己）clone 下來，`npm install` 完就該能啟動——不需要先架 redis、不需要 docker compose 起一排容器。`CACHE_DRIVER` 與 `LOCK_DRIVER` 預設 `memory`，redis 完全不在場：連 redis client 都只在設定了 `REDIS_URL` 時才註冊，下游模組用 optional 注入，redis 缺席是**非事件**，健康檢查端點也只在它在場時才多一個指標。\n\n「開發環境的擬真度」與「上手的摩擦力」是一組交換——我選擇把摩擦力壓到零，用 port 抽象保證擬真度的債之後還得起。\n\n## 但 memory 鎖有一個致命邊界\n\nin-memory 的鎖是一個 `Map`——**它只存在於單一程序的記憶體裡**。PM2 cluster 開兩個 worker、或 k8s 跑兩個 pod，兩邊各自持有「同一把鎖」，互斥蕩然無存。[[jobs-outbox|jobs worker]] 靠鎖保證多實例下不重複執行，這個場景下 memory 鎖等於沒鎖。\n\n所以規則寫得很死：**任何非單程序的部署，`LOCK_DRIVER` 必須是 redis**。這種「開發時安全、部署時炸」的陷阱，文件寫三遍都不嫌多——它不會在任何測試裡現形，只會在生產環境的併發裡現形。\n\n## Port 抽象的複利\n\n因為所有消費者只認識 `ICacheService` \u002F `ILockService`，切換 driver 是純設定變更、零程式碼改動。[[scheduler-observability|排程觀測]]寫進快取後跨 pod 一致性自動跟上，就是這個抽象付的複利——**好的邊界讓後來的功能免費繼承正確性**。","平台的快取與分散式鎖各自抽象成 port，背後兩套實作：in-memory 與 redis，用環境變數切換。這個設計的核心信念是：fresh clone 必須零外部依賴就能跑起來。 預設 memory 的理由 新人（或三個月後的自己）clone 下來，npm install 完就該能啟動——不需要先架 redis、不需…",[9,10,11],"nestjs","平台","架構","budding",null,"2026-07-14T06:54:33.136Z","2026-07-14T11:26:37.715Z",true,{"jobs-outbox":18,"scheduler-observability":19},"Jobs queue 兼職 transactional outbox：一張表兩個身分","Cron 的可觀測性：排程不該是黑箱",[],"AI助理"]