前言
在最近的專案開發中,我一直在思考一個問題:對於中等規模的應用,我們真的需要 Redis 嗎?
雖然 Redis 在極致性能和特定的數據結構上無可替代,但引入它也帶來了額外的運維負擔和數據一致性挑戰。往往為了實現一個簡單的「訂單支付後更新快取」或「非同步發送通知」,我們需要在兩個系統(PostgreSQL 和 Redis)之間處理數據同步。
受 I replaced Redis with PostgreSQL and it’s faster 的啟發,我整理並實現了一套完全基於 PostgreSQL 的 Redis 替代方案。本文將從原理、維護到監控,系統性地分享這套「簡化架構」的落地細節。
核心模塊實現與維護細節
1. 快取 (Cache):UNLOGGED 表與 TTL 策略
原理與實現:
使用 UNLOGGED 表 繞過 WAL 日誌,提升寫入性能。這種表在資料庫崩潰後會清空,非常適合存放「丟了也無妨」的快取數據。我們通過 expires_at 欄位實現 TTL。
維護建議:
- 主動清理比 autovacuum 更重要:快取表通常寫入頻繁,依賴預設的 20% 死行觸發 autovacuum 可能導致表表膨脹過快。
- pg_cron 腳本:建議每 10 分鐘執行一次清理與
VACUUM。在高頻寫入場景,應採用分批刪除(使用LIMIT)以避免長時間鎖表。
-- 分批清理過期數據
DELETE FROM cache WHERE ctid IN (
SELECT ctid FROM cache WHERE expires_at < NOW() LIMIT 10000
);
VACUUM cache;
監控指標: 關注 pg_stat_user_tables 中的 n_dead_tup(死行數)和表大小。
2. 发布/訂閱 (Pub/Sub):LISTEN / NOTIFY
原理與實現: 利用 PG 內置通訊機制。通知是在事務提交時發出的,這確保了「數據已落庫」與「通知已發出」的原子性。
實踐細節:
- Payload 限制:
NOTIFY的 Payload 上限為 8KB。我強烈建議採用**「信號模式」**:只發送 ID,由訂閱者回表查詢。 - 連線保活:
LISTEN需要長連接。如果使用pgbouncer,必須設置為session模式或直連。 - 監控:通過
pg_notification_queue_usage()監控通知隊列占用率,防止突發大量通知導致隊列溢出。
3. 任務對列 (Queue):FOR UPDATE SKIP LOCKED
原理與實現:
通過 FOR UPDATE SKIP LOCKED 實現多 Worker 併發領取任務,有效避免領取衝突和死鎖。
進階維護:
- 超時回收機制:增加
locked_at和visibility_timeout欄位。對於「已領取但長時間未完成」的任务,需要有後臺任務定期回收(重置狀態)。 - 死信對列 (DLQ):對於達到
max_attempts的任務,應移入jobs_dead_letter表進行人工介入處理。 - 索引優化:必須對
(queue_name, scheduled_at)建立複合索引。
4. 會話管理 (Sessions):JSONB 與事務邊界
原理與實現: 將 Session 數據作為 JSONB 存儲。相比 Redis,PG Session 的殺手鐧是事務一致性:你可以在同一個事務內完成「訂單創建」和「更新 Session 中的最後活躍時間」。
優化策略:
- 數據約束:雖然 JSONB 靈活,但建議在應用層做 Schema 校验,避免數據漂移。
- 索引:對於頻繁查詢的 JSON 字段,可以建立 GIN 索引 或表達式索引來提升性能。
5. 限流 (Rate Limit):原子 UPSERT
原理與實現:
利用 INSERT ... ON CONFLICT DO UPDATE 在單條 SQL 中完成「判斷-計數-更新」的閉環。
INSERT INTO rate_limits (key, count, window_start)
VALUES ($1, 1, NOW())
ON CONFLICT (key) DO UPDATE
SET count = CASE
WHEN rate_limits.window_start < NOW() - INTERVAL '1 minute' THEN 1
ELSE rate_limits.count + 1
END,
window_start = CASE
WHEN rate_limits.window_start < NOW() - INTERVAL '1 minute' THEN NOW()
ELSE rate_limits.window_start
END;
監控告警: 對於極端熱點的 Key(如某些接口的全局限流),需要監控 pg_locks 中的行鎖競爭(Lock Contention)。
方案總結:何時「升級」?
這套方案並非銀彈。當你的業務增長到以下階段時,建議考慮專業組件:
- 消息可靠性要求極高:如果不能接受任何因斷線導致的
NOTIFY遺失,建議升級到 PGMQ 或 Kafka。 - 極高併發:當吞吐量超過 10k+ ops/s,或者對延遲要求在亞毫秒級(< 1ms)時,請回歸 Redis。
- 複雜數據結構:如果你需要 Redis 的有序集合(ZSET)、流(Streams)或位圖(Bitmaps)功能。
結語
速度不是唯一的衡量標准,系統的可推理性(System Reasoning)才是。
通過將狀態聚合在 PostgreSQL 的事務邊界內,我們極大地減少了分布式系統中「中間狀態」引發的 Bug。這是我所推崇的「極簡開發」。
具體的代碼實現與壓測數據,請參考我的開源項目:spike014/just-use-PostgreSQL。