告别Redis!如何在PostgreSQL 18实现无缓存架构的平滑落地?

Atomic Architect 2025-10-20 18:19:00
“我在生产环境关掉了 Redis,而且没有回滚。”

 

这句话现在仍让我心里发紧,因为缓存就像一条安全带。但在 PostgreSQL 18 上经过慎重试验后,p95 降低了,系统变简单了,值班负担也减轻了。

 

风险是真实的:用户会话、结账流程、我们的口碑。

收益也是真实的:更少的活动部件,更少的告警短信。

 

下面是我走过的路径、我修复的查询、唯一关键的图,以及几个起决定作用的小配置。

 

 

一、我拨动开关的那一天

 

我们的热路径是一个参数化的产品查询,并带有一点个性化。

 

缓存为我们带来了命中,但未命中很嘈杂,而且序列化增加了工作量。

 

我对该路径进行了端到端的分析,然后尝试了一个直接的数据库方案,使用更紧凑的索引和一个预先计算好的投影。

 

第一次运行很吓人——然后数据稳定下来了。

 

我让缓存停用了一个完整的周期,看着图表平静下来。

 

二、Postgres 里到底发生了什么

 

没有玄学,只有两件落地的事:

 

 
1、覆盖度与选择性

 

一个覆盖索引 + 一个生成列,让优化器只拿我们要的字段,不再碰堆表。

 

 
2、预计算形态

 

物化视图(并发刷新)把原来靠缓存藏起来的昂贵聚合扛了过来。

 

PostgreSQL 18 用更理智的执行计划 + 可预测的 I/O 完成了其余工作。

 

三、那个让 Redis 变得多余的查询

 

下面的表结构反映了一条常见的 Feed 或目录切片:商品、软删除、新鲜度、个性化键。

 

重点不是字段名,而是“生成列 + 覆盖索引”的组合正好匹配返回结果。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
-- Schema and plan helpers (run in a maintenance window)CREATE TABLE catalog_item (  item_id           BIGSERIAL PRIMARY KEY,  category_id       BIGINT NOT NULL,  tenant_id         BIGINT NOT NULL,  price_cents       INTEGER NOT NULL,  rating_avg        NUMERIC(3,2NOT NULL DEFAULT 0.0,  tags              TEXT[] NOT NULL DEFAULT '{}',  updated_at        TIMESTAMPTZ NOT NULL DEFAULT now(),  deleted           BOOLEAN NOT NULL DEFAULT FALSE);
-- Generated column to precompute a simple personalization bucketALTER TABLE catalog_item  ADD COLUMN p_bucket SMALLINT GENERATED ALWAYS AS (    (rating_avg * 10)::smallint  ) STORED;
-- Covering index that matches the query (note the INCLUDE list)CREATE INDEX CONCURRENTLY idx_catalog_lookup  ON catalog_item (tenant_id, category_id, p_bucket, deleted)  WHERE deleted = FALSE  INCLUDE (item_id, price_cents, rating_avg, updated_at, tags);
-- A precomputed projection for “trending” that used to be cachedCREATE MATERIALIZED VIEW mv_trending ASSELECT tenant_id, category_id,       item_id, price_cents, rating_avg, tags,       row_number() OVER (PARTITION BY tenant_id, category_id ORDER BY rating_avg DESC, updated_at DESCAS rkFROM catalog_itemWHERE deleted = FALSE;
-- Keep it fresh without blocking writersCREATE UNIQUE INDEX CONCURRENTLY mv_trending_pk  ON mv_trending (tenant_id, category_id, item_id);
-- In a job runner or cron:-- REFRESH MATERIALIZED VIEW CONCURRENTLY mv_trending;

 

应用程序调用随后变成了单次往返:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
-- Serve top N without touching RedisPREPARE fetch_slice (bigintbigintsmallintintASSELECT item_id, price_cents, rating_avg, tagsFROM   catalog_itemWHERE  tenant_id = $1  AND  category_id = $2  AND  p_bucket >= $3  AND  deleted = FALSEORDER  BY rating_avg DESC, updated_at DESCLIMIT  $4;
-- When trending is requestedPREPARE fetch_trending (bigintbigintintASSELECT item_id, price_cents, rating_avg, tagsFROM   mv_trendingWHERE  tenant_id = $1 AND category_id = $2 AND rk <= $3;

 

简而言之:规划器停留在索引上,堆保持"冷却",视图从热路径中移除了沉重的聚合操作。

 

四、从源头削减延迟

 

两个小的配置调整起到了作用。它们并非万能药;但它们确实让 I/O 保持稳定,并让规划器能够可靠地利用索引和并行性。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
# postgresql.conf (16 GB VM example)shared_buffers = '4GB'effective_cache_size = '11GB'work_mem = '64MB'maintenance_work_mem = '1GB'track_io_timing = onjit = onmax_worker_processes = 8max_parallel_workers_per_gather = 2random_page_cost = 1.1default_statistics_target = 500

 

如果你在更快的本地 NVMe 上运行,较低的 random_page_cost 能保持索引扫描的吸引力。track_io_timing 会在你对自己磁盘性能判断有误时告诉你。

 

五、我是如何测量的

 

我运行了一个简单的客户端,它发出了我们通常缓存的那组参数。下表来自我的环境,3 次预热运行,显示的是中位数。网络跳数和序列化开销比人们预期的要多。

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
+--------------------------------------+-------+-------+-------+-------------------------------+| Path                                 |  p50  |  p95  |  p99  | Notes                         |+--------------------------------------+-------+-------+-------+-------------------------------+| Redis (hit)                          |  6 ms |  18 ms|  28 ms| fast but extra hop            || Redis (miss → DB)                    | 24 ms |  80 ms| 120 ms| hop + serialization + origin  || Postgres 18 direct (covering index)  | 18 ms |  55 ms|  95 ms| fewer hops, stable tail       || Postgres 18 via mv_trending (warm)   | 12 ms |  38 ms|  70 ms| precomputed hot slice         |+--------------------------------------+-------+-------+-------+-------------------------------+

 

在我们的信息流端点上,直接的数据库路径击败了缓存未命名的尾部延迟,并消除了命中与未命中之间的断崖式差距。那个差距过去常常出现在用户追踪记录和支持工单中。

 

六、前后对比,用 ASCII 图绘制

 

一个说服团队的流程图:

 

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
BEFORE┌────────┐    ┌──────────────┐    ┌───────────┐│ Client │───▶│ Redis/Codec  │───▶│ Postgres  │└────────┘    └──────────────┘    └───────────┘    ▲              │                  ▲    └──── misses ──┘                  └── invalidates ─┘AFTER┌────────┐    ┌──────────────────────────────────────────┐│ Client │─────▶│              Postgres 18                │└────────┘    │  • Covering index (tenant, category, p)  │              │  • Materialized view for trending        │              │  • Parallel plan where it helps          │              └──────────────────────────────────────────┘

 

我们仍然保留了一个只读副本以确保安全,但热路径现在只有一个依赖项。

 

七、何时缓存仍然占优

 

我并不是反对缓存。如果你需要跨请求协调、速率窗口,或者跨服务的扇出扇入操作,缓存或消息总线仍然有其用武之地。我仅在它掩盖了规划器错误并增加了方差的地方移除了它。一个诚实的承认:我曾尝试通过降低 synchronous_commit 来榨取更多性能,但后来撤销了,因为对于这条路径来说,写入语义的风险不值得去冒。

 

八、我保留的可直接复用的更改

 

这些是实验结束后保留的两个应用层面的部分。

 

 
1、应用查询形态(SQL 预处理语句),而非 ORM 猜测

 

参数顺序稳定的预处理语句使得执行计划稳定且解析快速。它们也明确了需要索引什么。前面的代码块展示了 fetch_slice 和 fetch_trending;那就是实际部署的代码。

 

 
2、适度、常规的刷新节奏

 

刷新 mv_trending 的作业每隔几分钟运行一次,使用 CONCURRENTLY 以保持读取端的顺畅。没有复杂的 cron 技巧,没有失效风暴。

 

九、这对你的团队意味着什么

 

如果你的缓存只是为了掩盖源站慢读,请先尝试修好源站。

 

把查询盖全,只预计算真正昂贵的部分。

 

度量 p95,别看平均。

 

如果直接走数据库能打败缓存未命中的长尾,还能减少运维痛苦,就让更简单的架构赢。

 

作者丨The Atomic Architect    编译丨Rio

来源丨网址:https://medium.com/@the_atomic_architect/postgresql-18-killed-my-cache-layer-and-i-am-not-bringing-it-back-764496b2a9a5

 

 

*本文为dbaplus社群编译整理,如需转载请取得授权并标明出处!dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn

最新评论
访客 2024年04月08日

如果字段的最大可能长度超过255字节,那么长度值可能…

访客 2024年03月04日

只能说作者太用心了,优秀

访客 2024年02月23日

感谢详解

访客 2024年02月20日

一般干个7-8年(即30岁左右),能做到年入40w-50w;有…

访客 2023年08月20日

230721

活动预告